查看: 2662|回复: 5

[讨论] [原創] 關於MP3文檔格式的解剖

[复制链接]

签到天数: 711 天

连续签到: 44 天

[LV.9]以坛为家II

119

主题

5974

积分

1

支持

发表于 2019-1-8 16:33:39 来自手机 | 显示全部楼层 |阅读模式

马上注册,享受积分奖励和更多功能,让您轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?注册

x
本帖最后由 TonyDeng 于 2019-1-8 16:51 编辑

目前流行的MP3歌曲,一般是ID3 v2.3格式文檔,它由三部分組成:
1.頭部(Header)
2.一系列幀(Frame)信息,分別記錄相關的信息,比如專輯名稱、歌手、版權信息等等
3.音頻數據,這部分才是歌曲的真正數據,供播放器解碼播放。

對ID3格式的詳細説明,參考網址:http://id3.org/Home

下面是UWP框架下C#寫的解讀代碼:

    public class MP3
    {
        public StorageFile SongFile { get; set; }

        public Header Header { get; set; }
        public List<Frame> Frames { get; set; }
        public byte[] Audio { get; set; }

        public async Task Load()
        {
            using (Stream stream = (await SongFile.OpenAsync(FileAccessMode.ReadWrite, StorageOpenOptions.AllowReadersAndWriters)).AsStream())
            {
                byte[] buffer = new byte[stream.Length];
                if (await stream.ReadAsync(buffer, 0, buffer.Length) == buffer.Length)
                {
                    int index = 0;
                    Header = new Header(buffer, ref index);
                    Frames = new List<Frame>();
                    FrameTag frameTag = new FrameTag(buffer, ref index);
                    while (!frameTag.IsEmpty)
                    {
                        Frame frame = new Frame(buffer, ref index, frameTag);
                        Frames.Add(frame);
                        frameTag = new FrameTag(buffer, ref index);
                    }
                    index = Header.TagSize + Header.Size;
                    Audio = new byte[stream.Length - index];
                    Array.Copy(buffer, index, Audio, 0, Audio.Length);
                }
            }
        }

        public async Task Update()
        {
            Header.Size = (Frames.Count + 1) * FrameTag.TagSize + Frames.Sum(f => f.Tag.Size);
            using (Stream stream = (await SongFile.OpenAsync(FileAccessMode.ReadWrite, StorageOpenOptions.AllowReadersAndWriters)).AsStream())
            {
                await stream.WriteAsync(Header.GetBytes(), 0, Header.TagSize);
                foreach (Frame frame in Frames)
                {
                    await stream.WriteAsync(frame.Tag.GetBytes(), 0, FrameTag.TagSize);
                    await stream.WriteAsync(frame.GetBytes(), 0, frame.Tag.Size);
                }
                await stream.WriteAsync(new byte[FrameTag.TagSize], 0, FrameTag.TagSize);
                await stream.WriteAsync(Audio, 0, Audio.Length);
                stream.SetLength(stream.Position);
            }
        }
    }


    public class Header
    {
        public static int TagSize => 10;

        public string ID { get; set; }          // MP3文檔的標識,必須為“ID3“
        public byte Ver { get; set; }           // 版本號,若為3則表示V2.3版本
        public byte Revision { get; set; }      // 副版本號,通常為零
        public byte Flag { get; set; }          // 附加標識,bit[0]為非同步編碼標識,bit[1]為擴展標簽頭標識,bit[2]為測試標識
        public int Size { get; set; }           // 頭部數據尺寸

        public bool IsAvailable => (ID == “ID3“) && (Ver == 3) && (Size > 0);

        public Header()
        {
            ID = ““;
            Ver = 0;
            Revision = 0;
            Size = 0;
        }

        public Header(byte[] buffer, ref int startIndex) : this()
        {
            ID = Encoding.ASCII.GetString(buffer, startIndex, 3);
            Ver = buffer[startIndex + 3];
            Revision = buffer[startIndex + 4];
            Flag = buffer[startIndex + 5];
            Size = (buffer[startIndex + 6] << 7 << 7 << 7) + (buffer[startIndex + 7] << 7 << 7) + (buffer[startIndex + 8] << 7) + buffer[startIndex + 9];
            startIndex += TagSize;
        }

        public byte[] GetBytes()
        {
            Encoding.ASCII.GetBytes(ID, 0, ID.Length, _bytes, 0);
            _bytes[3] = Ver;
            _bytes[4] = Revision;
            _bytes[5] = Flag;
            _bytes[6] = (byte)((Size >> 7 >> 7 >> 7) & 0x7F);
            _bytes[7] = (byte)((Size >> 7 >> 7) & 0x7F);
            _bytes[8] = (byte)((Size >> 7) & 0x7F);
            _bytes[9] = (byte)(Size & 0x7F);
            return _bytes;
        }

        public override string ToString()
        {
            return $“{ID}v2.{Ver}.{Revision} {Convert.ToString(Flag, 2)} {Size:N0}Bytes“;
        }

        private byte[] _bytes = new byte[TagSize];
    }

這裏關於頭部尺寸Size的計算公式,才是正確的。在網上比如CSDN上有大量的討論,基本都栽在這個計算上,沒幾個是正確的,所以不斷地有人反饋解讀錯誤,無法達成目的。

另外,對幀集合的讀取,有一點必須注意:每個幀之間是沒有明確分界標志的,必須逐個從其幀頭標簽中提取後隨的幀體尺寸,依次讀入。判斷幀表結束的標志與C/C++的傳統做法一樣,就是最後會遇到一個空標簽(10個字節的0)。關鍵之處,有些編碼器是不止寫入10個字節的空白區域,我見過有寫入4K字節空白的,所以,解讀的時候,千萬不要自以爲是讀到空白的後面就是音頻數據了,真正的音頻數據開始區域,必須根據頭部Header中Size尺寸指示來尋找。在我這個代碼中,當自己重寫文檔的時候,統一把這部分空白截成10個字節,免得浪費空間。

一些MP3歌曲,裏面不單有v2.3的信息,還可能有v1的信息。v1這部分好辦,它必定在文檔最尾部的128個字節處,定位到那裏查找有沒有v1的標簽信息就可以了。
来自:MS-7982 WIN10 PC版客户端

该用户从未签到

0

主题

-57

积分

7

支持

VIP

Rank: 6Rank: 6

积分
-57
发表于 2019-1-8 18:11:51 | 显示全部楼层
@WFun_ljj9898发帖:
[你知道吗]:

签到天数: 1247 天

连续签到: 336 天

[LV.10]以坛为家III

4

主题

6069

积分

3

支持

UWP初心者,智机网膜法师

发表于 2019-1-8 18:34:13 来自手机 | 显示全部楼层
标记马克,以后些许用得上。
[你知道吗]:

签到天数: 340 天

连续签到: 37 天

[LV.8]以坛为家I

115

主题

4593

积分

12

支持

发表于 2019-1-8 20:09:14 来自手机 | 显示全部楼层
本帖最后由 太学主 于 2019-1-8 20:47 编辑

感谢分享。

来自:Lumia 920 -智机社区客户端 WP8.1

签到天数: 711 天

连续签到: 44 天

[LV.9]以坛为家II

119

主题

5974

积分

1

支持

 楼主| 发表于 2019-1-8 22:55:53 来自手机 | 显示全部楼层
HavokPro 发表于 2019-1-8 18:34
标记马克,以后些许用得上。

有點潔癖。下載了許多歌曲,其中有不少是信息不全的,或者是不統一,還有一些歌曲是從無損中轉換格式而來(320K的MP3音質跟無損人耳基本上無法分辨,但體積小許多可以帶在手機上),逐首歌整理是不現實的(Groove雖然也能編輯歌曲信息但那個界面其實不好用),就想寫個程序成批處理。後續的想法,是把歌詞(可以是同步或異步)嵌入到MP3中,供支持的播放器顯示歌詞,其實按現有的資料,自己做播放器顯示歌詞也是可以的了。國行的Groove,並不支持在OneDrive上播放歌曲,自己做的應該可以(還有一點小問題需要解決)。
来自:MS-7982 WIN10 PC版客户端
[你知道吗]:

签到天数: 209 天

连续签到: 32 天

[LV.7]常住居民III

4

主题

340

积分

6

支持

发表于 2019-1-9 04:10:22 来自手机 | 显示全部楼层
同马克一下,以后说不定会用。

嵌入歌词的歌曲,各家播放器原创的格式见过不少,但是真正下功夫去修改拓展 mp3 的不多,楼主加油

来自:Lumia 950 XL Win10旗舰-智机社区客户端
您需要登录后才可以回帖 登录 | 注册

本版积分规则

        

网站地图| 小黑屋|京ICP证150706号|京B2-20160045|京网文[2018]3705-313号| 京公网安备11010802018258号

Powered by Discuz! X3.4 / Copyright 2010-2017 © 智机网 WFUN.COM Inc. All rights reserved.

快速回复 返回顶部 返回列表