首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

音视频学习 - MP3格式

  • 25-04-23 00:21
  • 4638
  • 6248
juejin.cn

环境

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和自定义的部分

java
代码解读
复制代码
// 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-33存放”TAG”字符,表示ID3V1.0标准,紧接其后的是歌曲信息
4-3330歌名
34-6330作者
64-9330专辑名
94-974年份
98-12730附注
1281MP3音乐类别,共147种

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

ini
代码解读
复制代码
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";
验证
java
代码解读
复制代码
// Main.java public class Main { public static void main(String[] args) throws InvalidDataException, UnsupportedTagException, IOException { Main parse = new Main(); String filename = "resource/v24tagswithalbumimage.mp3"; ... } }
java
代码解读
复制代码
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

java
代码解读
复制代码
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; } } }
java
代码解读
复制代码
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); } } }
java
代码解读
复制代码
// 最后调用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必存在
c
代码解读
复制代码
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文件时必须计算该值以便找到相邻的帧

计算公式:

scss
代码解读
复制代码
Layer2/3:Len(字节) = ((每帧采样数/8*比特率)/采样频率)+填充

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

帧持续时间

计算公式:

scss
代码解读
复制代码
每帧持续时间(毫秒) = 每帧采样数 / 采样频率 * 1000

例:1152/441000*1000=26ms

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

验证

读取音频数据帧头

java
代码解读
复制代码
// Mp3File.java // 初始化方法 ... // 传MP3文件 scanFile(seekableByteChannel); ...
java
代码解读
复制代码
// 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> 获取文件偏移量

java
代码解读
复制代码
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; }

规则代码如下

java
代码解读
复制代码
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>音频首帧的处理

java
代码解读
复制代码
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; }

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

java
代码解读
复制代码
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"帧

java
代码解读
复制代码
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>读取音频帧

java
代码解读
复制代码
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字节

c
代码解读
复制代码
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个字节的帧头和至少一个字节的内容构成

c
代码解读
复制代码
// 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

java
代码解读
复制代码
... 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会加上

sh
代码解读
复制代码
$ 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根据上面提到的公式

c
代码解读
复制代码
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的内容。

寻找首个音频帧

ini
代码解读
复制代码
// 根据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

参考

  1. 音频格式之MP3:(1)MP3封装格式简介
  2. 静态码率(CBR)和动态码率(VBR)
  3. MP3文件结构解析(超详细)
注:本文转载自juejin.cn的忘川三的文章"https://juejin.cn/post/7495938507211767848"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

142
代码人生
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top