概述
在上一篇文章中我们编写了代码来更改 wave 文件的幅度。
现在,我们将看一下如何通过调节声像将单声道 wave 文件转换为立体声 wave 文件,并探索 WAVE 文件格式如何在内部表示该文件。
频道
WAVE 文件中的原始音频数据由多个帧组成。目前,我们称它们为“样本”,尽管严格来讲这并不完全正确。实际上,当我们假设一个单声道音频文件时,原始音频数据中的单个浮动仅对应于一个样本。
当你有多个频道时,单个“样本”可以包含多个帧。由于每个频道都需要在任何给定的时间点播放特定的“帧”。
在 WAVE 文件格式中,频道是交错的。例如,立体声文件的布局应如下所示:
在这里,每个样本都由两个帧组成。这样,1 和 2 构成样本 1,3 和 4 构成样本 2,依此类推。
程序由于音频文件中的fmt
块而知道如何解析原始音频数据,这些块指定了原始音频数据中存在的频道数。wave 文件中的最大频道数实际上高达 65,536,对于音频数据而言实际上没有任何意义。
一些常见的是:
- 1 频道:单声道
- 2 频道:立体声
- 3 频道:立体声+中央声道
- 4 频道:四声道
- 5 频道:“环绕声”
为了方便起见,我们主要处理单声道和立体声文件。它们不仅是最常用的,而且还使我们可以更方便的测试代码。
音频调节(Panning)
那么什么是 pan?平移音频信号时,实际上是在左侧或右侧使音频信号“更大声”。通常在DAW[1]中由“自动化轨道”表示,其值-1 到 1 之间的。
应用平底锅的程序将采用三个参数:
- 输入文件
- 输出文件
- 音频调节(-1 至 1)
对于输入文件,我们将其限制为单声道文件,对于输出文件,我们将生成立体声文件。pan 变量应在-1(左)和 1(右)之间。在开始应用 pan 之前,我们需要从输入 wave 文件中读取原始音频数据。请记住,要读取 wave 文件,我们将使用我们之前制作的GoAudio[2]库:
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。要了解为什么不完美,我们需要首先引入断点作为创建自动化跟踪的一种方式,因此我们接下来的几篇文章的重点将是断点。:-)
参考资料
[1]DAW: https://en.wikipedia.org/wiki/Digital_audio_workstation
[2]GoAudio: https://github.com/DylanMeeus/GoAudio
最后
以上就是霸气短靴为你收集整理的左右声道测试音频_[译]使用Go播放音频:立体声的全部内容,希望文章能够帮你解决左右声道测试音频_[译]使用Go播放音频:立体声所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复