美文网首页
使用Go播放音频:波形分析

使用Go播放音频:波形分析

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

原文地址: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头原则上存在于三个组成部分 chunkIDchunk sizeformat。对于我们的wave文件,格式应始终是WAVE。以下是wave头的示意图,其中标注了字节偏移量以及以位为单位的大小。

image

FMT

除了头之外,wave文件还包含两个子块。一种是元数据,它描述了文件中的音频数据的表现方式。第二个子块包含原始音频数据本身。

以下是“ fmt”的表示:

image

简要解释一下,它包含:

  • 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
}

在这里,我们可以看到如何检查头中的RIFFWAVE内容,以确保它们以正确的形状显示。

也许更关键的是,我们需要读取原始音频数据。

// 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声音文件。

相关文章

网友评论

      本文标题:使用Go播放音频:波形分析

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