美文网首页
使用Go播放音频:立体声

使用Go播放音频:立体声

作者: 豆腐匠 | 来源:发表于2020-09-15 18:55 被阅读0次

原文地址: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之间的。

应用平底锅的程序将采用三个参数:

  • 输入文件
  • 输出文件
  • 音频调节(-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。要了解为什么不完美,我们需要首先引入断点作为创建自动化跟踪的一种方式,因此我们接下来的几篇文章的重点将是断点。:-)

相关文章

网友评论

      本文标题:使用Go播放音频:立体声

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