使用Go播放音频:立体声

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

在上一篇文章中我们编写了代码来更改wave文件的幅度。

现在,我们将看一下如何通过调节声像将单声道wave文件转换为立体声wave文件,并探索WAVE文件格式如何在内部表示该文件。

频道

WAVE文件中的原始音频数据由多个帧组成。目前,我们称它们为“样本”,尽管严格来讲这并不完全正确。实际上,当我们假设一个单声道音频文件时,原始音频数据中的单个浮动仅对应于一个样本。

当你有多个频道时,单个“样本”可以包含多个帧。由于每个频道都需要在任何给定的时间点播放特定的“帧”。

在WAVE文件格式中,频道是交错的。例如,立体声文件的布局应如下所示:

image

在这里,每个样本都由两个帧组成。这样,1和2构成样本1,3和4构成样本2,依此类推。

程序由于音频文件中的fmt块而知道如何解析原始音频数据,这些块指定了原始音频数据中存在的频道数。wave文件中的最大频道数实际上高达65,536,对于音频数据而言实际上没有任何意义。

一些常见的是:

  • 1频道:单声道
  • 2频道:立体声
  • 3频道:立体声+中央声道
  • 4频道:四声道
  • 5频道:“环绕声”

为了方便起见,我们主要处理单声道和立体声文件。它们不仅是最常用的,而且还使我们可以更方便的测试代码。

音频调节(Panning)

那么什么是pan?平移音频信号时,实际上是在左侧或右侧使音频信号“更大声”。通常在DAW中由“自动化轨道”表示,其值-1到1之间的。

应用pan的程序将采用三个参数:

  • 输入文件
  • 输出文件
  • 音频调节(-1至1)

对于输入文件,我们将其限制为单声道文件,对于输出文件,我们将生成立体声文件。pan变量应在-1(左)和1(右)之间。在开始应用pan之前,我们需要从输入wave文件中读取原始音频数据。请记住,要读取wave文件,我们将使用我们之前制作的GoAudio库:

import (
        wav "github.com/DylanMeeus/GoAudio/wave"
)

该程序的设置非常简单,我们将使用内置flags程序包来解析CLI的输入。

var (
    input  = flag.String("i", "", "input file")
    output = flag.String("o", "", "output file")
    pan    = flag.Float64("p", 0.0, "pan in range of -1 (left) to 1 (right)")
)

设置好标志后,我们就可以解析它们并读取输入文件。

func main() {
    flag.Parse()
    infile := *input
    outfile := *output
    panfac := *pan
    wave, err := wav.ReadWaveFile(infile)
    if err != nil {
        panic("Could not parse wave file")
    }
        ...
}

到目前为止,一切都很好。我们已经解析了输入,因此我们知道要为pan使用哪个值,并且还读取了原始音频数据。但是,如何从(-1)到(1)范围内的值变为左侧或右侧的实际响度变化?我们可以想象一个简单的函数看起来像下面这样:

type panposition struct {
    left, right float64
}

func calculatePosition(position float64) panposition {
    position *= 0.5
    return panposition{
        left:  position - 0.5,
        right: position + 0.5,
    }
}

在这里,我们使用的结构可以代表左声道和右声道的幅度在0到1的范围内。这样我们观察到以下值:

位置 左声道 右声道
0 0.5 0.5
1 0 1
-1 1 0

换句话说,如果位置为零,则声音在耳机的左侧和右侧之间达到完美平衡。而在极值中,声音只能是左侧或右侧。

就像上一篇文章一样,我们实际上需要根据在calculatePosition函数中找到的位置数据来更改帧。我们可以创建一个函数,该函数根据上一个函数中panposition返回的值修改帧。

func applyPan(frames []wav.Frame, p panposition) []wav.Frame {
    out := []wav.Frame{}
    for _, s := range frames {
        out = append(out, wav.Frame(float64(s)*p.left))
        out = append(out, wav.Frame(float64(s)*p.right))
    }
    return out
}

请注意,我们如何实际将两个frame附加到frames结果切片上!这就是我们交错左右声道的方式。

现在我们可以完成main方法:

        ...
    pos := calculatePosition(panfac)
    scaledFrames := applyPan(wave.Frames, calculatePosition(panfac))
    wave.NumChannels = 2 // samples are now stereo, so we need dual channels
    if err := wav.WriteFrames(scaledFrames, wave.WaveFmt, outfile); err != nil {
        panic(err)
    }

这里至关重要的一步是,在编写样本之前,我们已经运行了wave.NumChannels=2。否则,wave文件将被解释为单声道声音文件,而我们的声像效果将会丢失。

测试代码

为了测试,我主要使用这个简单的mono文件

如果运行go run main.go -i mono.wav -o left-side.wav -p -1,将得到:

left-side.wav

当我们运行时,go run main.go -i mono.wav -o right-side.wav -p 1我们得到:

right-side.wav

下一步?

我们正在使用的pan功能实际上存在一个缺陷。但是,对于我们来说,这还不是很明显,因为我们只为整个音频源设置pan。要了解为什么不完美,我们需要首先引入断点作为创建自动化跟踪的一种方式,因此我们接下来的几篇文章的重点将是断点。:-)

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