当前位置: 首页 > news >正文

音视频学习 - 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)) {// 加载Id3v1initId3v1Tag(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-33存放”TAG”字符,表示ID3V1.0标准,紧接其后的是歌曲信息
4-3330歌名
34-6330作者
64-9330专辑名
94-974年份
98-12730附注
1281MP3音乐类别,共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必存在
CRC2可能存在
Side Info32必存在
声音数据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;
名称位长第几字节说明
同步信息111~2所有位均为1,第1字节恒为FF
版本2200-MPEG 2.5 01-未定义 10-MPEG2 11-MPEG 1
2200-未定义 01-Layer 3 10-Layer 2 11-Layer 1
CRC校验120-校验 1-不校验
位率43取样率,单位为kbs。详见下表
采样频率23MPEG-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-未定义
帧长调节13用于调整文件头长度,0:无需调整 1:调整
保留字13没有使用
声道模式2400:立体声Stereo 01:Joint Stereo 10:双声道 11:单声道
扩充模式24当声道模式为01时才使用
版权140:不合法 1:合法
原版标志14是否原版, 0: 非原版,1:原版
强调方式24用于声音降噪压缩后再补偿的分类,很少用到

位率

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
0000freefreefreefreefreefree
000132323232(32)32(8)8(8)
001064484064(48)48(16)16(16)
001196564896(56)56(24)24(24)
01001286456128(64)64(32)32(32)
01011608064160(80)80(40)64(40)
01101929680192(96)96(48)80(48)
011122411296224(112)112(56)56(56)
1000256128112256(128)128(64)64(64)
1001288160128288(144)160(80)128(80)
1010320192160320(160)192(96)160(96)
1011352224192356(176)224(112)112(112)
1100384256224384(192)256(128)128(128)
1101416320256416(224)320(144)256(144)
1110448384320448(256)384(160)320(160)
1111badbadbadbadbadbad

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

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的结构体的sizereturn 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要等于E0if (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 {// 非VBRstartOffset = 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();// 帧数量加1frameCount++;// 帧率添加到集合中,如果有不同多个值说明是VBRaddBitrate(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字节的帧头初始化MpegFrameMpegFrame 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 = 31473147 = 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

参考

  1. 音频格式之MP3:(1)MP3封装格式简介
  2. 静态码率(CBR)和动态码率(VBR)
  3. MP3文件结构解析(超详细)

相关文章:

  • 鸿蒙开发:Swiper轮播图
  • 【Rust】基本概念
  • Anaconda3使用conda进行包管理
  • 支持AVX2指令的计算机,ONNX推理量化模型比推理浮点模型慢?
  • 基于javaweb的SSM教材征订与发放管理系统设计与实现(源码+文档+部署讲解)
  • 携程-酒旅-数据研发面经【附答案】
  • go语言中defer使用指南
  • 逻辑思维:从混沌到秩序的理性推演在软件开发中的应用
  • 使用Nacos 打造微服务配置中心
  • Go语言之sync包 WaitGroup的使用和底层实现
  • 文件操作函数
  • 基于cubeMX的hal库STM32实现硬件IIC通信控制OLED屏
  • 汽车VIN码识别:解锁汽车行业的智能密码
  • Spark-SQL 项目
  • 爬虫(requests库,logging库)
  • react 父子组件通信 子 直接到父, 父 forwardref子
  • window上 elasticsearch v9.0 与 jmeter5.6.3版本 冲突,造成es 启动失败
  • 关于在Springboot中设置时间格式问题
  • Git -> Git 所有提交阶段的回滚操作
  • 测试-时间规模化定律可以改进世界基础模型吗?
  • 《哪吒之魔童降世》电影版权方诉《仙侠神域》游戏运营方侵权案开庭
  • 何立峰出席跨境贸易便利化专项行动部署会并讲话
  • 消费者买国外电话卡使用时无信号,店铺:运营商故障,较少见
  • 看展览|深濑昌久:一位超现实主义摄影者的三种意象
  • 2024年度全国十大考古新发现公布,武王墩一号墓等入选
  • 人民日报:外资车企携新车、前沿技术亮相上海车展,坚定信心深耕中国市场