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

原文地址: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声音文件。

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