V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Misakas
V2EX  ›  程序员

研究音频格式时发现了一个关于 MP3 的冷知识, VLC 等各种软件竟然都没有遵循 mp3 规范的?

  •  3
     
  •   Misakas ·
    Mitsuha · 197 天前 · 2714 次点击
    这是一个创建于 197 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近在写一个支持读取多种音频格式元数据的库(又在造轮子了),研究到了比较古老的 MP3 ,分享一下

    概览一下 MP3 格式,大概是如下结构

    | ID3 v2 metadata | frame data | frame data | frame data ... | ID3 V1 metadata |
    

    metadata 用来储存歌手、专辑等信息,然后后面跟一段一段音频帧,就储存了实际的音乐内容

    音频帧详细的规范在这里定义 MP3' Tech - Frame header。简单概述一下大概是如下格式

    AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
    

    开头的 13 个 bit A 代表帧的开始,要求都被填充为 1 ,并且说明这 13 个 bit 可能出现在数据中的任意位置,当解码器接收到 13 个 1 就要开始尝试解码(此要求是为了方便流式传输中,只要接收到一段音频帧就可以开始播放)

    但问题来了,13 也不是 8 的整倍数,编程语言一般最低只能操作 byte ,再往下想操作 bit 就要使用位运算来搞动作了,而这个 Frame header 可能以各种奇怪的型状出现,比如:

    11111111 11111000	// 正好在两个 byte 起始位置
    00111111 11111110	// 在两个 byte 之间
    0111111 111111101	// 在两个 byte 之间,但起始位在第二位,并且最后还跟了一个其他的数据位
    00011111 11111111	// 在最后一个 byte 的末尾
    00000111 11111111 11010101	// 横跨 nmd 三个 byte ,并且后面还跟其他数据位
    

    这解析起来难度上天了啊(摔桌子),并且 header 一跨 byte 边界后面的数据位都要跨边界。

    看到这里直接崩溃,随感觉这么逆天的东西不会真的有人支持了吧。于是就写了一个小 demo ,把一个 mp3 文件整体位移了一位

    // 示例代码
    all, err := io.ReadAll(origin)
    if err != nil {
    	panic(err)
    }
    
    data := make([]byte, 0, len(all))
    for i := 0; i < len(all); i++ {
    	temp := (all[i] & 1) << 7
    	if i+1 < len(all) {
    		temp |= all[i+1] >> 1
    	}
    	data = append(data, temp)
    }
    
    _, err = modified.Write(data)
    
    

    现在的 frame header 处于这样的位置

    00000001 11111111 11110000
    

    然后用 Windows 自带的播放器打开。结果时长直接读取错误,音频无法播放,

    怀疑是不是兼容性不行,播放界老大哥 VLC 一定会遵循规范的对不对!

    然后上 VLC 发现,VLC 直接崩了,时长都不读取

    等于是大家都嫌按 bit 位来定位 frame header 太麻烦,如果不是在 byte 开头就都放弃了。MPEG 出的规范没一个遵守的

    13 条回复    2024-05-09 08:58:02 +08:00
    weyou
        1
    weyou  
       197 天前 via Android   ❤️ 1
    所谓的流式传输一般不是指比特流,都是字节流。所以只有第一种是符合的
    013231
        2
    013231  
       197 天前   ❤️ 1
    我认为你理解错了。

    “The frame header itself is 32 bits (4 bytes) length. The first twelve bits (or first eleven bits in the case of the MPEG 2.5 extension) of a frame header are always set to 1 and are called "frame sync". ”

    frame sync 是 11 或 12 位,而且它们必须出现在 frame header 的开头,不能是任意位置。
    Misakas
        3
    Misakas  
    OP
       197 天前
    @weyou 忽略这个问题了,所以实际上也不会出现跨越边界的情况
    Misakas
        4
    Misakas  
    OP
       197 天前
    @013231 我知道他必须出现在开头,但 frame header 前面还有 ID3 tag ,我怕 ID3 tag 的数据不是 8 的倍数,导致 后面跟的 frame header 跨 byte 边界。不过 #1 的老哥也说了一般都是按字节流算, 所以我确实理解有问题。
    mercury233
        5
    mercury233  
       197 天前
    应用层处理的不太可能有比特流吧
    subframe75361
        6
    subframe75361  
       197 天前 via Android
    好巧,我也在造元数据的轮子,不过是巨人肩膀上的小草😂 https://github.com/subframe7536/music-metadata-wasm
    0xD800
        7
    0xD800  
       197 天前
    支持 silk 吗 把 silk 转 pcm 的
    Donaldo
        8
    Donaldo  
       197 天前
    @Misakas #3 感觉哪怕真的是比特流也没什么大问题,只需要一个缓冲区就好了。
    0o0O0o0O0o
        9
    0o0O0o0O0o  
       197 天前
    前段时间做个音频流相关的玩具,一开始用 mp3 ,最后还是换成了 ogg ,感觉 mp3 对编程太不友好了
    nightwitch
        10
    nightwitch  
       196 天前
    借用 Linus 的一句名言,"Standards are paper. I use paper to wipe my butt every day. That's how much that paper is worth."
    mayli
        11
    mayli  
       196 天前   ❤️ 2
    @Misakas 一般不会不是 8 的倍数,程序员不会自己没事找事。
    cppgohan
        12
    cppgohan  
       196 天前
    感觉是楼主想多了? 看你发的链接, 我的理解就是直接按 frame head 本身去解就行了. 没有什么 8bit 对齐的问题, 直接解 32 位 frame head 然后你就知道这个 frameblock 哪里结束, 直接处理就好了.

    这个 "frame sync" 在我看来对于应用层, 只是一个 magic number.
    cslive
        13
    cslive  
       196 天前
    你想多了,你去 id3tag 那个网站,上面列举了好多库,java,go ,c 的都有,找一个你喜欢的看看别人怎么理解规范的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3157 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 12:22 · PVG 20:22 · LAX 04:22 · JFK 07:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.