原文地址:https://dylanmeeus.github.io/posts/audio-from-scratch-pt2/
在上一篇文章中我们研究了如何创建简单的二进制声音文件。通过创建具有指数衰减的正弦波,我们可以获得单个音符弹奏的效果。
现在我们知道这些类型的文件是什么样的。但是,在现实世界中,你通常会遇到更复杂的文件。查找音频的常见格式之一是WAVE文件格式,通常以扩展名.wav
或.wave
表示。
在这篇文章中,我们将学习如何从该文件中提取信息,以及如何将自己的音频数据写入wave文件。这将为以后的帖子打下基础,我们可以在其中开始处理这些音频数据。
所有代码都可以在GitHub上找到,尽管这次是在单独的库中。如果你想知道如何操作音频本身而不是处理文件的编写,那么下一篇文章会介绍这一点。
WAVE中有什么?
WAVE是“ WaveForm音频文件格式”,由IBM和Microsoft在90年代初共同开发。波形文件将音频数据作为样本(双精度)与元数据一起存储,这些元数据描述了期望从音频流中获取的内容,例如声道数量(单声道,立体声,环绕声..),样本数量等。
通常,编码使用PCM,脉冲编码调制。虽然并不需要立即理解这些,但该代码将在各个地方引用PCM。为了简单起见,我们将假设每个文件都是这样编码的。
剖析
让我们逐步分解wave文件的内容。
头部
wave文件的第一部分是头。以十六进制查看,你可以通过突出的“ RIFF”和“ WAVE”值来识别。例如,当我打开wave文件的hex输出时,这是hex编辑器的第一行:
00000000: 5249 4646 1028 0200 5741 5645 666d 7420 RIFF.(..WAVEfmt
这一行告诉我们一些有用的事情。首先,RIFF表示它是使用little-endian编写的,否则它将是RIFX。其次,WAVE和fmt消息告诉我们至少文件的部分是正确生成的。
wave头原则上存在于三个组成部分 chunkID
, chunk size
和 format
。对于我们的wave文件,格式应始终是WAVE
。以下是wave头的示意图,其中标注了字节偏移量以及以位为单位的大小。
FMT
除了头之外,wave文件还包含两个子块。一种是元数据,它描述了文件中的音频数据的表现方式。第二个子块包含原始音频数据本身。
以下是“ fmt”的表示:
简要解释一下,它包含:
- Subchunk1ID:应包含
fmt
。 - Subchunk1Size:fmt块的大小
- AudioFormat:通常为PCM,由值
1
表示 - NumChannels:1 =单声道,2 =立体声,..
- SampleRate:44100、48000,..
- ByteRate:SampleRate * NumChannels *(BitsPerSample / 8)
- BitsPerSample:8、16、32 ...
代码部分
wave读取
读取和写入WAVE文件的代码的关键部分都是关于如何将我们的bit数据转换为实际数据的。我们将不得不根据数据块字段将数据转换为浮点数或整数。
为了将一个4字节的片段写入int,我们可以使用以下代码:
// turn a 32-bit byte array into an int
func bits32ToInt(b []byte) int {
if len(b) != 4 {
panic("Expected size 4!")
}
var payload uint32
buf := bytes.NewReader(b)
err := binary.Read(buf, binary.LittleEndian, &payload)
if err != nil {
panic(err)
}
return int(payload) // easier to work with ints
}
接下来,我们也可以将其用于浮点数。
func bitsToFloat(b []byte) float64 {
var bits uint64
switch len(b) {
case 2:
bits = uint64(binary.LittleEndian.Uint16(b))
case 4:
bits = uint64(binary.LittleEndian.Uint32(b))
case 8:
bits = binary.LittleEndian.Uint64(b)
default:
panic("Can't parse to float..")
}
float := math.Float64frombits(bits)
return float
}
然后,使用这些功能,我们可以将它们合并为实际的阅读器。
func readHeader(b []byte) WaveHeader {
hdr := WaveHeader{}
chunkID := b[0:4]
hdr.ChunkID = b[0:4]
if string(hdr.ChunkID) != "RIFF" {
// Validation of the header file
panic("Invalid file")
}
chunkSize := b[4:8]
var size uint32
buf := bytes.NewReader(chunkSize)
err := binary.Read(buf, binary.LittleEndian, &size)
if err != nil {
panic(err)
}
hdr.ChunkSize = int(size) // easier to work with ints
format := b[8:12]
if string(format) != "WAVE" {
panic("Format should be WAVE")
}
hdr.Format = string(format)
return hdr
}
在这里,我们可以看到如何检查头中的RIFF
和WAVE
内容,以确保它们以正确的形状显示。
也许更关键的是,我们需要读取原始音频数据。
// Should we do n-channel separation at this point?
func parseRawData(wfmt WaveFmt, rawdata []byte) []Sample {
bytesSampleSize := wfmt.BitsPerSample / 8
// TODO: sanity-check that this is a power of 2? I think only those sample sizes are
// possible
samples := []Sample{}
// read the chunks
for i := 0; i < len(rawdata); i += bytesSampleSize {
rawSample := rawdata[i : i+bytesSampleSize]
sample := bitsToFloat(rawSample)
samples = append(samples, Sample(sample))
}
return samples
}
所有块都遵循类似的模式,都可以在GitHub上找到
写入
对于写入,用于读取的关键功能就是类型转换了。我们将一个int或float转换为一个字节片。
要将int32写入字节:
func int32ToBytes(i int) []byte {
b := make([]byte, 4)
in := uint32(i)
binary.LittleEndian.PutUint32(b, in)
return b
}
同样,我们可以编写float64:
func floatToBytes(f float64, nBytes int) []byte {
bits := math.Float64bits(f)
bs := make([]byte, 8)
binary.LittleEndian.PutUint64(bs, bits)
// trim padding
switch nBytes {
case 2:
return bs[:2]
case 4:
return bs[:4]
}
return bs
}
这里最关键的部分是编写原始音频样本,这些辅助函数如下所示:
// Turn the samples into raw data...
func samplesToRawData(samples []Sample, props WaveFmt) []byte {
raw := []byte{}
for _, s := range samples {
bits := floatToBytes(float64(s), props.BitsPerSample/8)
raw = append(raw, bits...)
}
return raw
}
下一步是什么?
现在我们有了这个库,下一篇文章可以深入探讨如何使用它来处理.wave声音文件。