使用Go播放音频:断点

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

音频断点

为了清除潜在的错误,此博客文章是关于音频文件的断点,而不是我们用于调试代码的断点;-)。断点文件构成了DAWs中通常称为“ envelopes”或“automation tracks”的基础。

它们是包含timestamp:value数据对的简单文件。通过这种简单的结构,它们使我们可以指定声音文件在某些​​时间点应具有的某些属性。

例如,下面是我在FL Studio中制作的一个自动化轨道的屏幕截图,由Toto制作:


image.png

在这里您可以比较两个声音文件(使用耳机更明显):

在这篇文章中,我们将仔细讨论断点文件的原理。

例如,可以将pan编码为:

时间(秒)
0 1
5 -1
10 0
13.37 0.55

当第一个值编码时间时,第一个值必须是严格上升的值。但是,第二个值取决于你究竟要“自动化”什么。在我们的左右音频调节的情况下,我们可以使用介于-1到1之间的值对其进行编码。正如我们在上一篇文章中看到的那样,这将使我们能够以合适的方式修改样本以获得这种效果。但是请注意,我们的音频平移功能还不是完美的-因此我们不会获得与FL Studio完全相同的结果。以后再说。

线性插值

你可能已经注意到,断点文件中的时间值不必与样本相同地增加时间。这样非常好,因为我们可以缩小文件的大小,但是我们不希望音频在某个特定的时刻从左向右“跳跃”。如果我们对值进行编码:

时间(秒)
0 1
5 -1

我们并不是说“从1开始,在第二个5处跳到-1”。我们实际上要说的是“从1开始,逐渐减小到-1”。解决这个问题的工具是线性插值

因此,现在只需找出断点文件中我们正在处理的样本中的两个值。因此,如果我们的样本在歌曲中播放了2.5秒钟,我们发现该值必须介于第二0和第二个5之间的一半,从而使我们有一个声像0(左右音频完全平衡)。这些值05我们将称为 span

秘诀

(照常,所有代码都可以在Github上找到。

有了断点文件的背景信息,我们还牢记如何处理断点文件的方法。我们的断点模块需要执行几个步骤:

  • 读取断点文件
  • 将字符串解析为“时间-值”对
  • 给定一帧时间,找到它之间的跨度
  • 使用线性插值法查找确切值

会有一些边缘情况需要处理,但这只是我们所需要的粗略概述。

解析断点文件

我们应该实现的第一个功能是将实际文件解析为断点的功能。我们假设断点将作为文件传递,但实际上我们将对此进行抽象,而只接受一个io.Reader

我们的断点时间值参数将编码为time:value。尽管可以根据需要使用其他分隔符,但这只是对代码的微小调整。

首先,我们可以定义断点类型:

type Breakpoint struct {
        Time, Value float64
}

因此,我们从io.Reader中获取一些输入,然后将其解析为单独的行。然后,对于每一行,我们在分隔符(:)上分割并将这些值转换为float64值。这些被捆绑到我们的Breakpoint结构体上,然后添加到一个[]Breakpoint切片中,我们将返回给用户。这段代码可以使用一些切片边界检查,但是通过这种方式,本文更容易阅读

func ParseBreakpoints(in io.Reader) ([]Breakpoint, error) {
    data, err := ioutil.ReadAll(in)
    if err != nil {
        return nil, err
    }

    lines := strings.Split(string(data), "\n")

    brkpnts := []Breakpoint{}
    for _, line := range lines {
        line = strings.TrimSpace(line)
        if line == "" {
            continue
        }
        parts := strings.Split(line, ":")
        time := parts[0]
        value := parts[1]

        tf, err := strconv.ParseFloat(time, 64)
        if err != nil {
            return brkpnts, err
        }
        vf, err := strconv.ParseFloat(value, 64)
        if err != nil {
            return brkpnts, err
        }

        brkpnts = append(brkpnts, Breakpoint{
            Time:  tf,
            Value: vf,
        })

    }
    return brkpnts, nil
}

寻找正确的值

断点模块的另一个重要部分是在给定断点和请求时间的情况下实际返回一个值。如前所述,这将通过对给定范围内找到的值进行线性插值来完成。我们需要考虑的第一个边缘情况:如果我们的数据点位于最后一项会怎样?在这种情况下,不需要插值,我们只返回最后一个值。

第一步将是找到正确的跨度。我们可以遍历所有“时间-值”对,直到超出Time部分为止,从而知道我们看到的最后一个时间是跨度的“开始”。

func ValueAt(bs []Breakpoint, time float64, startIndex int) (index int, value float64) {
   if len(bs) == 0 {
       return 0, 0
   }
   npoints := len(bs)

   // first we need to find a span containing our timeslot
   startSpan := startIndex // start of span
   for _, b := range bs[startSpan:] {
       if b.Time > time {
           break
       }
       startSpan++
   }
       ...

通过此代码,我们找到了正确的startSpan,并且在没有传递断点的情况下,我们还有一条小的保护语句。

可以在这里处理第一个边缘情况,如果我们startSpan的片段大于切片中的断点数量,则可以返回循环中遇到的最后一个断点的值。

    // Our span is never-ending (the last point in our breakpoint file was hit)
    if startSpan == npoints {
        return startSpan, bs[startSpan-1].Value
    }

现在我们已经处理了这种边缘情况,实际上我们可以检索到一个跨度。我们碰到了第二个极端情况,如果断点的两次相同,该怎么办?想象一下:

时间(秒)
5 1
5 -1

在这种情况下,我们必须返回与最后一个条目关联的值。因此,在我们的示例中,结果将为-1。如果用户希望在开始的5秒钟内逐渐将值增加到1,然后立即跳到-1。如果两个时间戳之间的“距离”为零,则可以检测到。

    // check for instant jump
    width := right.Time - left.Time

    if width == 0 {
        return startSpan, right.Value
    }

最后,我们通过了“边缘情况”,并最终到达了插值部分。我们可以使用上面计算出的width来完成此功能:

    frac := (time - left.Time) / width
    val := left.Value + ((right.Value - left.Value) * frac)
    return startSpan, val

太好了,有了这个,我们所有人都将开始在我们的第一个自动化音轨上工作!

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