云计算百科
云计算领域专业知识百科平台

音视频学习 - MP3格式

环境

JDK 13 IDEA Build #IC-243.26053.27, built on March 16, 2025

Demo

MP3Parser

MP3

MP3全称为MPEG Audio Layer 3,它是一种高效的计算机音频编码方案,它以较大的压缩比将音频文件转换成较小的扩展名为.mp3的文件,基本保持源文件的音质,MP3是ISO/MPEG标准的一部分,

ISO/MPEG标准描述了使用高性能感知编码方案的音频压缩,此标准一直在不断更新以满足“质高量小”的追求,现已形成MPEG Layer1、Layer2、Layer3三种音频编解码方案,分别对应MP1、MP2、MP3 这三种声音文件

了解下MP3的编码方式

静态码率(CBR):Constants Bits Rate是一种固定采样率的压缩方式 这种编码方式不需要文件头,第一帧开始就是音频数据

(1)优点:压缩快,能被大多数软件和设备支持。 (2)缺点:占用空间大,效果不是十分理想。现已逐渐被VBR方式取代。

动态码率(VBR):Variable Bit Rate使用这个方式时,可以选择从最差音质/最大压缩比到最好音质/最低压缩比之间的种种过渡级数,在MP3文件编码之时,程序会尝试保持所选定的整个文件的品质,将选择适合音乐文件不同部分的不同比特率来编码。

需要文件头

(1)优点:可以让整首歌都能大致达到我们的音质要求。 (2)缺点:编码时无法估计压缩出来的文件体积大小

文件结构

一般可分为以下三部分

来自参考3

17452054380250.jpg

配合具体的解析的开源工程来理解

mp3agic-Java写的读写mp3的开源库

因为工程是以前的,遇到些问题,费了些时间,编译出mp3agic的jar包

Snip20250421_1.png

查看该工具加载MP3文件代码,从中也可以看出,分成Id3v1,音频,Id3v2和自定义的部分

// Mp3File.java
private void init(int bufferLength, boolean scanFile) throws IOException, UnsupportedTagException, InvalidDataException {
if (bufferLength < MINIMUM_BUFFER_LENGTH + 1) throw new IllegalArgumentException("Buffer too small");

this.bufferLength = bufferLength;
this.scanFile = scanFile;

try (SeekableByteChannel seekableByteChannel = Files.newByteChannel(path, StandardOpenOption.READ)) {
// 加载Id3v1
initId3v1Tag(seekableByteChannel);

// 加载音频帧部分
scanFile(seekableByteChannel);
if (startOffset < 0) {
throw new InvalidDataException("No mpegs frames found");
}

// 加载Id3v2部分
initId3v2Tag(seekableByteChannel);
if (scanFile) {
// 加载自定义部分
initCustomTag(seekableByteChannel);
}
}
}

ID3V1

ID3 V1.0标准并不周全,存放的信息少,无法存放歌词,无法录入专辑封面、图片等。

此标准是将MP3文件尾的最后128个字节用来存放ID3信息

字节长度(字节)说明
1-3 3 存放”TAG”字符,表示ID3V1.0标准,紧接其后的是歌曲信息
4-33 30 歌名
34-63 30 作者
64-93 30 专辑名
94-97 4 年份
98-127 30 附注
128 1 MP3音乐类别,共147种

音乐类型具体可以看 ID3v1Geners.java中定义的枚举或者本文后的参考3

0="Blues";
1="ClassicRock";
2="Country";
3="Dance";
4="Disco";
5="Funk";
6="Grunge";
7="Hip-Hop";
8="Jazz";
9="Metal";
10="NewAge";
11="Oldies";
12="Other";
13="Pop";
14="R&B";
15="Rap";
16="Reggae";
17="Rock";
18="Techno";

143="Salsa";
144="Trashl";
145="Anime";
146="JPop";
147="Synthpop";

验证

// Main.java
public class Main {
public static void main(String[] args) throws InvalidDataException, UnsupportedTagException, IOException {
Main parse = new Main();
String filename = "resource/v24tagswithalbumimage.mp3";
...
}
}

public void getID3v1Tag (String filename) throws InvalidDataException, UnsupportedTagException, IOException {
System.out.println("\\n========================MP3 ID3v1============================");
Mp3File mp3file = new Mp3File(filename);
if (mp3file.hasId3v1Tag()) {
ID3v1 id3v1Tag = mp3file.getId3v1Tag();
System.out.println("Track: " + id3v1Tag.getTrack());
System.out.println("Artist: " + id3v1Tag.getArtist());
System.out.println("Title: " + id3v1Tag.getTitle());
System.out.println("Album: " + id3v1Tag.getAlbum());
System.out.println("Year: " + id3v1Tag.getYear());
System.out.println("Genre: " + id3v1Tag.getGenre() + " (" + id3v1Tag.getGenreDescription() + ")");
System.out.println("Comment: " + id3v1Tag.getComment());
}
}

2025-04-22 11.53.22.png

private void initId3v1Tag(SeekableByteChannel var1) throws IOException {
// 创建128个字节的大小缓冲区
ByteBuffer var2 = ByteBuffer.allocate(128);
// 文件对象指向尾部 – 128的位置
var1.position(this.getLength() 128L);
var2.clear();
// 读取128个字节
int var3 = var1.read(var2);
if (var3 < 128) {
throw new IOException("Not enough bytes read");
} else {
try {
// 创建id3vTag对象。将128个字节传入
this.id3v1Tag = new ID3v1Tag(var2.array());
} catch (NoSuchTagException var5) {
// 不是以TAG开头抛出异常在这里捕获,是否有Id3v1这个就是判断该属性
// public boolean hasId3v1Tag() {
// return this.id3v1Tag != null;
// }
this.id3v1Tag = null;
}
}
}

private void unpackTag(byte[] var1) throws NoSuchTagException {
// 是否以TAG开头
this.sanityCheckTag(var1);
// 这里就是根据协议读取指定区间,然后转成对应的内容
this.title = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 3, 30));
this.artist = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 33, 30));
this.album = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 63, 30));
this.year = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 93, 4));
// 音乐类型
this.genre = var1[127] & 255;
if (this.genre == 255) {
this.genre = 1;
}

// 读取附注
if (var1[125] != 0) {
this.comment = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 97, 30));
this.track = null;
} else {
this.comment = BufferTools.trimStringRight(BufferTools.byteBufferToStringIgnoringEncodingIssues(var1, 97, 28));
byte var2 = var1[126];
if (var2 == 0) {
this.track = "";
} else {
this.track = Integer.toString(var2);
}
}

}

// 最后调用String,然后编码格式是ISO-8859-1,应该不支持中文
public static String byteBufferToStringIgnoringEncodingIssues(byte[] var0, int var1, int var2) {
try {
return byteBufferToString(var0, var1, var2, defaultCharsetName);
} catch (UnsupportedEncodingException var4) {
return null;
}
}

音频帧

来自参考1

每个帧都有一个帧头,长度是四个字节,帧后面可能有2字节的CRC校验,取决于帧头的第16位,为0则无校验,为1则有校验,后面是可变长度的附加信息,对于标准的MP3文件来说,其长度是32字节,紧接其后的是压缩的声音数据,当解码器读到此处时就进行解码了。

名称长度(字节)属性
帧头 4 必存在
CRC 2 可能存在
Side Info 32 必存在
声音数据 N 必存在

typedef FrameHeader {
unsigned int sync:11; // 同步信息
unsigned int version:2; // 版本
unsigned int layer: 2; // 层
unsigned int error protection:1; // 是否要CRC校验
unsigned int bitrate_index:4; // 位率
unsigned int sampling_frequency:2; // 采样频率
unsigned int padding:1; // 帧长调节
unsigned int private:1; // 保留字
unsigned int mode:2; // 声道模式
unsigned int mode extension:2; // 扩充模式
unsigned int copyright:1; // 版权
unsigned int original:1; // 原版标志
unsigned int emphasis:2; // 强调模式
}HEADER, *LPHEADER;

名称位长第几字节说明
同步信息 11 1~2 所有位均为1,第1字节恒为FF
版本 2 2 00-MPEG 2.5 01-未定义 10-MPEG2 11-MPEG 1
2 2 00-未定义 01-Layer 3 10-Layer 2 11-Layer 1
CRC校验 1 2 0-校验 1-不校验
位率 4 3 取样率,单位为kbs。详见下表
采样频率 2 3 MPEG-1: 00:44.1kHz 01:48kHz 10:32kHz 11-未定义
MPEG-2: 00:22.05kHz 01:24kHz 10:16kHz 11-未定义
MPEG-2.5: 00:11.025kHz 01:12kHz 10:8kHz 11-未定义
帧长调节 1 3 用于调整文件头长度,0:无需调整 1:调整
保留字 1 3 没有使用
声道模式 2 4 00:立体声Stereo 01:Joint Stereo 10:双声道 11:单声道
扩充模式 2 4 当声道模式为01时才使用
版权 1 4 0:不合法 1:合法
原版标志 1 4 是否原版, 0: 非原版,1:原版
强调方式 2 4 用于声音降噪压缩后再补偿的分类,很少用到

位率

V1: MPEG 1 V2: MPEG 2 和 MPEG 2.5

L1: Layer 1 L2: Lyaer 2 L3: Layer 3

bitsV1,L1V1,L2V1,L3V2,L1V2,L2V2,L3
0000 free free free free free free
0001 32 32 32 32(32) 32(8) 8(8)
0010 64 48 40 64(48) 48(16) 16(16)
0011 96 56 48 96(56) 56(24) 24(24)
0100 128 64 56 128(64) 64(32) 32(32)
0101 160 80 64 160(80) 80(40) 64(40)
0110 192 96 80 192(96) 96(48) 80(48)
0111 224 112 96 224(112) 112(56) 56(56)
1000 256 128 112 256(128) 128(64) 64(64)
1001 288 160 128 288(144) 160(80) 128(80)
1010 320 192 160 320(160) 192(96) 160(96)
1011 352 224 192 356(176) 224(112) 112(112)
1100 384 256 224 384(192) 256(128) 128(128)
1101 416 320 256 416(224) 320(144) 256(144)
1110 448 384 320 448(256) 384(160) 320(160)
1111 bad bad bad bad bad bad

帧大小即每帧的采样数,表示一帧数据中采样的个数,该值是恒定的

MP3的帧大小是1152

帧长度是压缩时每一帧的长度,包括帧头的4个字节。它将填充的空位也计算在内。Layer 2和Layer 3的空位是1字节。当读取MPEG文件时必须计算该值以便找到相邻的帧

计算公式:

Layer2/3:Len(字节) = ((每帧采样数/8*比特率)/采样频率)+填充

例:MPEG1 Layer3 比特率128000,采样率44100,填充0,帧长度为:((1152/8*128K)/44.1K+0=417字节

帧持续时间

计算公式:

每帧持续时间(毫秒) = 每帧采样数 / 采样频率 * 1000

例:1152/441000*1000=26ms

帧头后边是Side Info。对标准的立体声MP3文件来说其长度为32字节。当解码器在读到上述信息后,就可以进行解码了

验证

读取音频数据帧头

// Mp3File.java
// 初始化方法
...
// 传MP3文件
scanFile(seekableByteChannel);
...

// Mp3File.java
private void scanFile(SeekableByteChannel seekableByteChannel) throws IOException, InvalidDataException {
// 读取MP3文件的缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferLength);
// <1>获取音频帧的起始位置
int fileOffset = preScanFile(seekableByteChannel);
seekableByteChannel.position(fileOffset);
boolean lastBlock = false;
int lastOffset = fileOffset;
while (!lastBlock) {
byteBuffer.clear();
int bytesRead = seekableByteChannel.read(byteBuffer);
byte[] bytes = byteBuffer.array();
// 最后一帧字节长度小于缓冲区长度
if (bytesRead < bufferLength) lastBlock = true;
if (bytesRead >= MINIMUM_BUFFER_LENGTH) {
while (true) {
try {
int offset = 0;
// <2>音频首帧的处理
if (startOffset < 0) {
offset = scanBlockForStart(bytes, bytesRead, fileOffset, offset);
if (startOffset >= 0 && !scanFile) {
return;
}
lastOffset = startOffset;
}
// <3>读取音频帧
offset = scanBlock(bytes, bytesRead, fileOffset, offset);
// 文件偏移量
fileOffset += offset;
// 更新文件的偏移量
seekableByteChannel.position(fileOffset);
break;
} catch (InvalidDataException e) {
if (frameCount < 2) {
startOffset = 1;
xingOffset = 1;
frameCount = 0;
bitrates.clear();
lastBlock = false;
fileOffset = lastOffset + 1;
if (fileOffset == 0)
throw new InvalidDataException("Valid start of mpeg frames not found", e);
seekableByteChannel.position(fileOffset);
break;
}
return;
}
}
}
}
}

<1> 获取文件偏移量

protected int preScanFile(SeekableByteChannel seekableByteChannel) {
ByteBuffer byteBuffer = ByteBuffer.allocate(AbstractID3v2Tag.HEADER_LENGTH);
try {
seekableByteChannel.position(0);
byteBuffer.clear();
int bytesRead = seekableByteChannel.read(byteBuffer);
// ID3v2的表头 10个字节
if (bytesRead == AbstractID3v2Tag.HEADER_LENGTH) {
try {
byte[] bytes = byteBuffer.array();
ID3v2TagFactory.sanityCheckTag(bytes);
// 音频帧的起始位置是通过表头的10个字节 + f(表头最后的4个字节)计算出来,后面可知就是ID3v2的结构体的size
return AbstractID3v2Tag.HEADER_LENGTH + BufferTools.unpackSynchsafeInteger(bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET], bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 1], bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 2], bytes[AbstractID3v2Tag.DATA_LENGTH_OFFSET + 3]);
} catch (NoSuchTagException | UnsupportedTagException e) {
// do nothing
}
}
} catch (IOException e) {
// do nothing
}
return 0;
}

规则代码如下

public static int shiftByte(byte c, int places) {
// c 位与 0xff 那还是原来的字节,只保留低8位
int i = c & 0xff;
// < 0 ,i向左位移
// > 0 , i向右位移,虽然这里传的都是<0,考虑大端的场景应该是兼容
if (places < 0) {
return i << places;
} else if (places > 0) {
return i >> places;
}
return i;
}

public static int unpackSynchsafeInteger(byte b1, byte b2, byte b3, byte b4) {
// b4是size的最低位 => size[3]
// b3是size[2]
// …
// & 0x7f就是只保留该位,然后通过位移得到该位的值,然后相加计算出ID3v2的总长度
int value = ((byte) (b4 & 0x7f));
value += shiftByte((byte) (b3 & 0x7f), 7);
value += shiftByte((byte) (b2 & 0x7f), 14);
value += shiftByte((byte) (b1 & 0x7f), 21);
return value;
}

<2>音频首帧的处理

private int scanBlockForStart(byte[] bytes, int bytesRead, int absoluteOffset, int offset) {
while (offset < bytesRead MINIMUM_BUFFER_LENGTH) {
// 音频帧头同步: 11位都要是1 = 0b1111 1111 1110 000。低位与E0要等于E0
if (bytes[offset] == (byte) 0xFF && (bytes[offset + 1] & (byte) 0xE0) == (byte) 0xE0) {
try {
// 初始化音频帧,帧头有4个字节
MpegFrame frame = new MpegFrame(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
// 因为VBR是Xing公司发明的,所以音频首帧里会留"痕迹",位于MP3文件中第一个有效帧的数据区
if (xingOffset < 0 && isXingFrame(bytes, offset)) {
xingOffset = absoluteOffset + offset;
xingBitrate = frame.getBitrate();
offset += frame.getLengthInBytes();
} else {
// 非VBR
startOffset = absoluteOffset + offset;
channelMode = frame.getChannelMode();
emphasis = frame.getEmphasis();
layer = frame.getLayer();
modeExtension = frame.getModeExtension();
sampleRate = frame.getSampleRate();
version = frame.getVersion();
copyright = frame.isCopyright();
original = frame.isOriginal();
// 帧数量加1
frameCount++;
// 帧率添加到集合中,如果有不同多个值说明是VBR
addBitrate(frame.getBitrate());
offset += frame.getLengthInBytes();
return offset;
}
} catch (InvalidDataException e) {
offset++;
}
} else {
offset++;
}
}
return offset;
}

这里按音频帧头的格式填充

public MpegFrame(byte frameData1, byte frameData2, byte frameData3, byte frameData4) throws InvalidDataException {
long frameHeader = BufferTools.unpackInteger(frameData1, frameData2, frameData3, frameData4);
setFields(frameHeader);
}

private void setFields(long frameHeader) throws InvalidDataException {
long frameSync = extractField(frameHeader, BITMASK_FRAME_SYNC);
if (frameSync != FRAME_SYNC) throw new InvalidDataException("Frame sync missing");
setVersion(extractField(frameHeader, BITMASK_VERSION));
setLayer(extractField(frameHeader, BITMASK_LAYER));
setProtection(extractField(frameHeader, BITMASK_PROTECTION));
setBitRate(extractField(frameHeader, BITMASK_BITRATE));
setSampleRate(extractField(frameHeader, BITMASK_SAMPLE_RATE));
setPadding(extractField(frameHeader, BITMASK_PADDING));
setPrivate(extractField(frameHeader, BITMASK_PRIVATE));
setChannelMode(extractField(frameHeader, BITMASK_CHANNEL_MODE));
setModeExtension(extractField(frameHeader, BITMASK_MODE_EXTENSION));
setCopyright(extractField(frameHeader, BITMASK_COPYRIGHT));
setOriginal(extractField(frameHeader, BITMASK_ORIGINAL));
setEmphasis(extractField(frameHeader, BITMASK_EMPHASIS));
}

判断是否是"Xing"帧

private boolean isXingFrame(byte[] bytes, int offset) {
if (bytes.length >= offset + XING_MARKER_OFFSET_1 + 3) {
if ("Xing".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_1, 4)))
return true;
if ("Info".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_1, 4)))
return true;
if (bytes.length >= offset + XING_MARKER_OFFSET_2 + 3) {
if ("Xing".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_2, 4)))
return true;
if ("Info".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_2, 4)))
return true;
if (bytes.length >= offset + XING_MARKER_OFFSET_3 + 3) {
if ("Xing".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_3, 4)))
return true;
if ("Info".equals(BufferTools.byteBufferToStringIgnoringEncodingIssues(bytes, offset + XING_MARKER_OFFSET_3, 4)))
return true;
}
}
}
return false;
}

<3>读取音频帧

private int scanBlock(byte[] bytes, int bytesRead, int absoluteOffset, int offset) throws InvalidDataException {
while (offset < bytesRead MINIMUM_BUFFER_LENGTH) {
// 同样传4字节的帧头初始化MpegFrame
MpegFrame frame = new MpegFrame(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
// 帧信息不一致容错处理,会抛异常
sanityCheckFrame(frame, absoluteOffset + offset);
int newEndOffset = absoluteOffset + offset + frame.getLengthInBytes() 1;
// 要给ID3v1留128个字节位置
if (newEndOffset < maxEndOffset()) {
endOffset = absoluteOffset + offset + frame.getLengthInBytes() 1;
frameCount++;
addBitrate(frame.getBitrate());
offset += frame.getLengthInBytes();
} else {
break;
}
}
return offset;
}

ID3V2

ID3V2一共有四个版本,ID3V2.1/2.2/2.3/2.4

ID3V2.3由一个标签头和若干个标签帧或者一个扩展标签头组成,至少要有一个标签帧,每一个标签帧记录一种信息,例如作曲、标题等

标签头

位于文件开始处,长度为10字节

char Header[3]; /*必须为“ID3”否则认为标签不存在*/
char Ver; /*版本号ID3V2.3 就记录3*/
char Revision; /*副版本号此版本记录为0*/

// 标志字节一般为0,定义如下(abc000000B)
// a:表示是否使用Unsynchronisation
// b:表示是否有扩展头部,一般没有,所以一般也不设置
// c:表示是否为测试标签,99.99%的标签都不是测试标签,不设置
char Flag; /*标志字节,只使用高三位,其它位为0 */

// 标签大小共四个字节,每个字节只使用低7位,最高位不使用恒为0,计算公式如下:
// Size = (Size[0] & 0x7F) * 0x200000 + (Size[1] & 0x7F) * 0x400+(Size[2] & 0x7F) * 0x80 + (Size[3] & 0x7F)
char Size[4]; /*标签大小*/

标签帧

每个标签帧都有10个字节的帧头和至少一个字节的内容构成

// TIT2: 标题5449 5432
// TPE1: 作者
// TALB: 专集
// TRCK: 音轨格式 N/M 其中N为专集中的第N首,M为专集中共M首,N和M 为ASCII 码表示的数字
// TYPE: 年代
// COMM: 备注,格式: "eng\\0备注内容",其中eng 表示备注所使用的自然语言
char ID[4]; /*标识帧,说明其内容,例如作者/标题等*/
// 帧内容大小,计算公式如下:
// Size = Size[0]*0x100000000 + Size[1]*0x10000+ Size[2]*0x100 +Size[3];
char Size[4]; /*帧内容的大小,不包括帧头,不得小于1*/
// 标志帧,使用每个字节的高三位,其他位均为0(abc00000B xyz00000B)
// a — 标签保护标志,设置时认为此帧作废
// b — 文件保护标志,设置时认为此帧作废
// c — 只读标志,设置时认为此帧不能修改
// x — 压缩标志,设置时一个字节存放两个BCD 码表示数字
// y — 加密标志
// z — 组标志,设置时说明此帧和其他的某帧是一组
char Flags[2]; /*标志帧,只定义了6 位*/

验证

打印MP3文件中的Id3v2

...
public void getID3v2Tag(String filename) throws InvalidDataException, UnsupportedTagException, IOException {
System.out.println("\\n========================MP3 ID3v2============================");
Mp3File mp3file = new Mp3File(filename);
if (mp3file.hasId3v2Tag()) {
ID3v2 id3v2Tag = mp3file.getId3v2Tag();
System.out.println("Track: " + id3v2Tag.getTrack());
System.out.println("Artist: " + id3v2Tag.getArtist());
System.out.println("Title: " + id3v2Tag.getTitle());
System.out.println("Album: " + id3v2Tag.getAlbum());
System.out.println("Year: " + id3v2Tag.getYear());
System.out.println("Genre: " + id3v2Tag.getGenre() + " (" + id3v2Tag.getGenreDescription() + ")");
System.out.println("Comment: " + id3v2Tag.getComment());
System.out.println("Lyrics: " + id3v2Tag.getLyrics());
System.out.println("Composer: " + id3v2Tag.getComposer());
System.out.println("Publisher: " + id3v2Tag.getPublisher());
System.out.println("Original artist: " + id3v2Tag.getOriginalArtist());
System.out.println("Album artist: " + id3v2Tag.getAlbumArtist());
System.out.println("Copyright: " + id3v2Tag.getCopyright());
System.out.println("URL: " + id3v2Tag.getUrl());
System.out.println("Encoder: " + id3v2Tag.getEncoder());
byte[] albumImageData = id3v2Tag.getAlbumImage();
if (albumImageData != null) {
System.out.println("Have album image data, length: " + albumImageData.length + " bytes");
System.out.println("Album image mime type: " + id3v2Tag.getAlbumImageMimeType());
}
}
}
...

结果如下

2025-04-22 10.46.41.png

从网上下载的一些mp3文件因为是静态码率,所以没有标签头,用Sublime Text打开后就是

2025-04-22 09.17.52.png

用FFmpeg把pcm压缩编码为mp3会加上

$ ffmpeg -y -i v24tagswithalbumimage.mp3 -acodec pcm_s16le -f s16le -ac 2 -ar 44100 v24tagswithalbumimage.pcm

v24tagswithalbumimage.mp3文件的二进制

标签头

Snip20250422_3.png

Header[3] + Version是4944 3304:就是 ID3v2和 第4版 Revision:0 Flag:0 不使用Unsynchronisation,没有扩展头,非测试标签

Size[4]是1841根据上面提到的公式

Size = (Size[0] & 0x7F) * 0x200000 + (Size[1] & 0x7F) * 0x400+(Size[2] & 0x7F) * 0x80 + (Size[3] & 0x7F)

计算: (0x18 & 0x7F) * 0x80 + (0x41 & 0x7F) = 0x18 * 0x80 + 0x41 = 3137

加上头的10个字节,所以mp3文件的ID3v2部分是 3147

这个调试的时候也可以验证

2025-04-22 11.36.58.png

var1 是mp3文件, var3 是被认为ID3v2的内容。

寻找首个音频帧

// 根据ID3v2的size + 上面的公式 + ID3v2的文件头
0x1841 => 0x41 + 0x18 << 7 = 3137
3137 + 10 = 3147

3147 = 196 * 16 + 11

2025-04-22 16.23.23.png

根据前4个字节 0xfffb9044

0b1111 1111 111/ 11 / 01 / 0 / 1001 / 00 / 0 / 0 / 01 / 00 0 / 1 / 00

同步信息: 0xfffb 前11个都是1 版本:11-说明是MPEG 1 层:01-说明是Layer 3 是MP3符合预期 CRC: 0 不校验 位率: 1001,因为是MPEG 1 + Layer 3,根据上面的码表 取样率:128kbps 采样率: 00,因为是MPEG 1说明是44.1kHz 帧长调节: 0,无需调整 保留字: 0 声道模式: 01 .Joint 关闭强度立体声 + MS立体声 扩充模式: 00 版权:0,不合法 原版:1,原版 强调方式: 00

参考

  • 音频格式之MP3:(1)MP3封装格式简介
  • 静态码率(CBR)和动态码率(VBR)
  • MP3文件结构解析(超详细)
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 音视频学习 - MP3格式
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!