音视频学习 - 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
配合具体的解析的开源工程来理解
mp3agic-Java写的读写mp3的开源库
因为工程是以前的,遇到些问题,费了些时间,编译出mp3agic的jar包
查看该工具加载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-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());}}
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
bits | V1,L1 | V1,L2 | V1,L3 | V2,L1 | V2,L2 | V2,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的结构体的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());}}}
...
结果如下
从网上下载的一些mp3文件因为是静态码率,所以没有标签头,用Sublime Text打开后就是
用FFmpeg把pcm压缩编码为mp3会加上
$ ffmpeg -y -i v24tagswithalbumimage.mp3 -acodec pcm_s16le -f s16le -ac 2 -ar 44100 v24tagswithalbumimage.pcm
v24tagswithalbumimage.mp3文件的二进制
标签头
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
这个调试的时候也可以验证
var1 是mp3文件, var3 是被认为ID3v2的内容。
寻找首个音频帧
// 根据ID3v2的size + 上面的公式 + ID3v2的文件头
0x1841 => 0x41 + 0x18 << 7 = 3137
3137 + 10 = 31473147 = 196 * 16 + 11
根据前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文件结构解析(超详细)
相关文章:
音视频学习 - MP3格式
环境 JDK 13 IDEA Build #IC-243.26053.27, built on March 16, 2025 Demo MP3Parser MP3 MP3全称为MPEG Audio Layer 3,它是一种高效的计算机音频编码方案,它以较大的压缩比将音频文件转换成较小的扩展名为.mp3的文件,基本保持源文件的音…...
Oracle--PL/SQL编程
前言:本博客仅作记录学习使用,部分图片出自网络,如有侵犯您的权益,请联系删除 PL/SQL(Procedural Language/SQL)是Oracle数据库中的一种过程化编程语言,构建于SQL之上,允许编写包含S…...
【愚公系列】《Python网络爬虫从入门到精通》063-项目实战电商数据侦探(主窗体的数据展示)
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! …...
DAPP(去中心化应用程序)开发全解析:构建去中心化应用的流程
去中心化应用(DApp)凭借其透明性、抗审查性和用户数据主权,正重塑金融、游戏、社交等领域。本文基于2025年最新开发实践,系统梳理DApp从需求规划到部署运维的全流程,并融入经济模型设计、安全加固等核心要点࿰…...
Spark与Hadoop之间有什么样的对比和联系
一、什么是Spark Spark 是一个快速、通用且可扩展的大数据处理框架,最初由加州大学伯克利分校的AMPLab于2009年开发,并于2010年开源。它在2013年成为Apache软件基金会的顶级项目,是大数据领域的重要工具之一。 Spark 的优势在于其速度和灵活…...
spark和Hadoop之间的对比和联系
Spark 诞生主要是为了解决 Hadoop MapReduce 在迭代计算以及交互式数据处理时面临的性能瓶颈问题。 一,spark的框架 Hadoop MR 框架 从数据源获取数据,经过分析计算后,将结果输出到指定位置,核心是一次计算,不适合迭…...
LeetCode 第 262 题全解析:从 SQL 到 Swift 的数据分析实战
文章目录 摘要描述题解答案(SQL)Swift 题解代码分析代码示例(可运行 Demo)示例测试及结果时间复杂度分析空间复杂度分析总结未来展望 摘要 在实际业务中,打车平台要监控行程的取消率,及时识别服务质量的问…...
“融合Python与机器学习的多光谱遥感技术:数据处理、智能分类及跨领域应用”
随着遥感技术的快速发展,多光谱数据凭借其多波段信息获取能力,成为地质、农业及环境监测等领域的重要工具。相较于高光谱数据,Landsat、哨兵-2号等免费中分辨率卫星数据具有长时间序列、广覆盖的优势,而无人机平台的兴起进一步补充…...
JavaScript的JSON处理Map的弊端
直接使用 Map 会遇到的问题及解决方案 直接使用 Map 会导致数据丢失,因为 JSON.stringify 无法序列化 Map。以下是详细分析及解决方法: 问题复现 // 示例代码 const myMap new Map(); myMap.set(user1, { name: Alice }); myMap.set(user2, { name: B…...
python的深拷贝浅拷贝(copy /deepcopy )
先说结论: 浅拷贝: 浅拷贝对在第一层的操作都是新建,不改变原对象。 浅拷贝对于原拷贝对象中的嵌套的可变对象是引用,对原拷贝对象中的嵌套的不可变对象是新建。 对新建的对象操作不会影响原被拷贝对象。 对引用对象操作会影…...
新能源汽车充电桩:多元化运营模式助力低碳出行
摘 要:以新能源汽车民用充电桩为研究对象,在分析充电桩建设运营的政府推动模式、电网企业推动模式、汽车厂商推动模式等三种模式利弊的基础上,结合我国的实际情况,提出我国现阶段应实行汽车厂商与电网企业联盟建设充电桩的模式。建立一个考虑…...
Python 设计模式:享元模式
1. 什么是享元模式? 享元模式是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。它特别适用于需要大量相似对象的场景,通过共享相同的对象来避免重复创建,从而节省内存和提高效率。 享元模式的核心思想是将对象的…...
文献×汽车 | 基于 ANSYS 的多级抛物线板簧系统分析
板簧系统是用于减弱或吸收动态系统中发生的应力、应变、偏转和变形等破坏性因素的机械结构。板簧系统可能对外力产生不同的响应,具体取决于其几何结构和材料特性。板簧系统的计算机辅助分析对于高精度确定系统的变形特性和结构特性至关重要。 在这项工作中ÿ…...
Element UI、Element Plus 里的表单验证的required必填的属性不能动态响应?
一 问题背景 想要实现: 新增/修改对话框中(同一个),修改时“备注”字段非必填,新增时"备注"字段必填 结果发现直接写不生效-初始化一次性 edit: [{ required: true, message: "请输入备注", trigger: "blur" }…...
【架构】ANSI/IEEE 1471-2000标准深度解析:软件密集型系统架构描述推荐实践
引言 在软件工程领域,架构设计是确保系统成功的关键因素之一。随着软件系统日益复杂化,如何有效描述和沟通系统架构成为了一个亟待解决的问题。ANSI/IEEE 1471-2000(正式名称为"推荐软件密集型系统架构描述实践")应运而…...
深度学习中的“重参数化”总结
深度学习中的重参数化(Reparameterization)是一种数学技巧,主要用于解决模型训练过程中随机性操作(如采样)导致的梯度不可导问题。其核心思想是将随机变量的生成过程分解为确定性和随机性两部分,使得反向传…...
为TA开发人员介绍具有最新改进的Kinibi-610a
安全之安全(security)博客目录导读 目录 一、引言 二、密码学改进 三、可信应用(TA)的多线程支持 四、C 标准库支持 五、简化的支持与集成 六、参考资料 一、引言 Trustonic 推出的 Kinibi-610a 进行了多项底层优化,以实现更深度的系统集成,并更好地适应不断演进的…...
通信与推理的协同冲突与架构解耦路径
在大规模无人机集群中,AI决策系统依赖实时通信完成状态共享与策略传播,但通信带宽、延迟、信息一致性等问题正在成为系统性能的瓶颈。尤其是在山区、城市低空或信号遮蔽等通信不稳定区域,AI推理系统往往面临状态更新延迟,难以及时…...
《AI大模型应知应会100篇》第32篇:大模型与医疗健康:辅助诊断的可能性与风险
第32篇:大模型与医疗健康:辅助诊断的可能性与风险 摘要 当AI开始读懂CT影像中的细微阴影,当算法能从百万份病历中发现诊断规律,医疗健康领域正经历着一场静默的革命。本文通过技术解构与案例分析,揭示大模型如何重塑临…...
c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译
c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译 【心法】 【第零章】c语言概述 【第一章】分支与循环语句 【第二章】函数 【第三章】数组 【第四章】操作符 【第五章】指针 【第六章】结构体 【第七章】const与c语言中一些错误代码 【禁忌秘术】 【第一式】…...
[创业之路-377]:企业法务 - 有限责任公司与股份有限公司的优缺点对比
有限责任公司(简称“有限公司”)与股份有限公司(简称“股份公司”)是我国《公司法》规定的两种主要公司形式,二者在设立条件、治理结构、股东权利义务等方面存在显著差异。以下从核心特征、设立条件、治理结构、股东权…...
PowerBi中REMOVEFILTERS怎么使用?
在 Power BI 的 DAX 中,REMOVEFILTERS() 是一个非常重要的函数,常用于取消某个字段或表的筛选上下文(Filter Context),从而让你的计算不受切片器(Slicer)、筛选器或视觉对象的限制。 ✅ 一、REM…...
stat判断路径
int stat(const char *pathname, struct stat *buf); pathname:用于指定一个需要查看属性的文件路径。 buf:struct stat 类型指针,用于指向一个 struct stat 结构体变量。调用 stat 函数的时候需要传入一个 struct stat 变量的指针࿰…...
智能指针之设计模式4
前面的文章介绍了使用工厂模式来封装智能指针对象的创建过程,下面介绍一下工厂类 enable_shared_from_this的实现方案。 4、模板方法模式 在前面的文章分析过,enable_shared_from_this<T>类是一个工厂基类,提供的工厂方法是shared_f…...
Linux信号的产生
Linux系列 文章目录 Linux系列一、信号的产生1.1 异常1.2 alarm()系统调用 二 、信号的默认行为 一、信号的产生 上篇文章我们已经介绍了信号的三种产生方式,这部分是对上篇文章的补充 1.1 异常 在编写程序时,我们的程序经常会出现如:除零…...
FPGA设计 时空变换
1、时空变换基本概念 1.1、时空概念简介 时钟速度决定完成任务需要的时间,规模的大小决定完成任务所需要的空间(资源),因此速度和规模就是FPGA中时间和空间的体现。 如果要提高FPGA的时钟,每个clk内组合逻辑所能做的事…...
客户端本地搭建
connect函数 主要用于客户端套接字向服务器发起连接请求。 头文件 #include <sys/socket.h> #include <arpa/inet.h> 函数原型 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数解释 sockfd:客户端文件描述符。 addr…...
广东食品销售初级考试主要考什么
广东省食品销售初级考试主要考察从业人员对食品安全法律法规、行业规范及基础操作技能的掌握程度,内容涵盖以下几个方面: 1. 食品安全法律法规 考试重点包括《食品安全法》《广东省食品安全条例》等核心法规,要求考生熟悉食品经营许可、从业…...
[盈达科技】GEO(生成式引擎优化)实战指南:从认知重构、技术落地到内容突围的三维战略
GEO(生成式引擎优化)实战指南:从认知重构、技术落地到内容突围的三维战略 引言:AI搜索重构规则,GEO成为企业新护城河 在生成式AI主导的搜索时代,传统SEO的“关键词游戏”已失效。Google数据显示࿰…...
Linux ACL访问控制权限解析:超越传统权限的精细化管理
Linux ACL权限管理示意图 标准输出:Linux文件权限结构(支持ACL) ├── 传统权限 │ ├── 所有者(user): rwx │ ├── 所属组(group): r-x │ └── 其他用户(other): r-- │ └── ACL扩展权限├── 用户条目│ ├── user…...
全面介绍AVFilter 的添加和使用
author: hjjdebug date: 2025年 04月 22日 星期二 13:48:19 CST description: 全面介绍AVFilter 的添加和使用 文章目录 1.两个重要的编码思想1. 写代码不再是我们调用别人,而是别人调用我们!2. 面向对象的编程方法. 2. AVFilter 开发流程2.1 编写AVFilter 文件2.1.…...
复刻低成本机械臂 SO-ARM100 3D 打印篇
视频讲解: 复刻低成本机械臂 SO-ARM100 3D 打印篇 清理了下许久不用的3D打印机,挤出机也裂了,更换了喷嘴和挤出机夹具,终于恢复了正常工作的状态,接下来还是要用起来,不然吃灰生锈了,于是乎想起…...
基于微信小程序的走失儿童帮助系统-项目分享
基于微信小程序的走失儿童帮助系统-项目分享 项目介绍项目摘要管理员功能图用户功能图系统功能图项目预览首页走失儿童个人中心走失儿童管理 最后 项目介绍 使用者:管理员、用户 开发技术:MySQLJavaSpringBootVue 项目摘要 本系统采用微信小程序进行开…...
C++23 中 static_assert 和 if constexpr 的窄化布尔转换
文章目录 背景与动机C23 的改进限制与例外总结 C23 引入了一项重要的语言特性变更,即在 static_assert 和 if constexpr 中允许窄化按语境转换为 bool。这一特性由 Andrzej Krzemieński 提出的 P1401R5 论文推动,旨在使编译器的行为与标准保持一致&a…...
服务网格在DevOps中的落地:如何让微服务更智能、更稳定?
服务网格在DevOps中的落地:如何让微服务更智能、更稳定? 近年来,DevOps在企业IT架构中变得至关重要,而微服务架构的广泛应用更是加速了这一趋势。然而,随着微服务数量不断增长,我们发现自己掉入了一个运维“泥潭”: 服务之间的流量调控变得复杂可观测性不足,出现问题时…...
el-table表格既出现横向滚动条,又出现纵向滚动条?
横向滚动条 自然出现? 当表格所有列的宽度总和超过表格容器宽度时,el-table会默认出现横向滚动条。 比如,给每个<el-table-column>设置固定宽度,且他们相加超过了<el-table>宽度 就会触发 强制出现? 设…...
STL常用算法——C++
1.概述 2.常用遍历算法 1.简介 2.for_each 方式一:传入普通函数(printf1) #include<stdio.h> using namespace std; #include<string> #include<vector> #include<functional> #include<algorithm> #include…...
基于国产 FPGA+ 龙芯2K1000处理器+翼辉国产操作系统继电保护装置测试装备解决方案
0 引言 近年来,我国自主可控芯片在国家政策和政 府的支持下发展迅速,并在电力、军工、机械、 通信、电子、医疗等领域掀起了国产化替代之 风,但在芯片自主可控和国产化替代方面还有明 显的不足之处。 2022年我国集成电路进口量多 达 5 3…...
1.3 本书结构概览:从理论基础到实践案例的系统阐述
本书采用由浅入深、理论联系实践的结构设计,旨在为读者提供一个关于大模型与智能代理(Agent)技术的全面认知框架与实施路径。全书共分为十章,系统性地覆盖了从技术基础到企业落地的完整知识链条,现概述如下: 首先,第一…...
【FPGA开发】Vivado开发中的LUTRAM占用LUT资源吗
LUTRAM在Vivado资源报告中的解释 LUTRAM的本质与实现原理: LUTRAM不是一种独立的物理资源,而是LUT(Look-Up Table)的一种特殊使用方式。在Xilinx FPGA架构中,部分LUT单元可以被配置为小型分布式RAM(也称为…...
【动手学强化学习】番外8-IPPO应用框架学习与复现
文章目录 一、待解决问题1.1 问题描述1.2 解决方法 二、方法详述2.1 必要说明(1)MAPPO 与 IPPO 算法的区别在于什么地方?(2)IPPO 算法应用框架主要参考来源 2.2 应用步骤2.2.1 搭建基础环境2.2.2 IPPO 算法实例复现&am…...
C++ 的 输入输出流(I/O Streams)
什么是输入输出流 C 的输入输出操作是通过 流(stream) 机制实现的。 流——就是数据的流动通道,比如: 输入流:从设备(如键盘、文件)读取数据 → 程序 输出流:程序将数据写入设备&…...
Java 安全:如何防止 SQL 注入与 XSS 攻击?
Java 安全:如何防止 SQL 注入与 XSS 攻击? 在 Java 开发领域,安全问题至关重要,而 SQL 注入和 XSS 攻击是两种常见的安全威胁。本文将深入探讨如何有效防止这两种攻击,通过详细代码实例为您呈现解决方案。 一、SQL 注…...
leetcode day36 01背包问题 494
494 目标和 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 或 - ,然后串联起所有整数,可以构造一个 表达式 : 例如,nums [2, 1] ,可以在 2 之前添加 ,在 1 之前添加 - &…...
31Calico网络插件的简单使用
环境准备: 1、删除Flannel 2、集群所有node节点拉取所需镜像(具体版本可以依据calico.yaml文件中): docker pull calico/cni:v3.25.0 docker pull calico/node:v3.25.0 docker pull calico/kube-controllers:v3.25.0一、安装Cali…...
进阶篇 第 5 篇:现代预测方法 - Prophet 与机器学习特征工程
进阶篇 第 5 篇:现代预测方法 - Prophet 与机器学习特征工程 (图片来源: ThisIsEngineering RAEng on Pexels) 在前几篇中,我们深入研究了经典的时间序列统计模型,如 ETS 和强大的 SARIMA 家族。它们在理论上成熟且应用广泛,但有…...
实用生活c语言脚本
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> // 清理临时文件目录 void clean_temp_directory() { const char* temp_dir "/tmp"; // 可自定义需要清理的目录 char command[1024]; …...
从零开始构建微博爬虫与数据分析系统
从零开始构建微博爬虫与数据分析系统 引言 社交媒体平台蕴含着海量的信息和数据,通过对这些数据的收集和分析,我们可以挖掘出有价值的见解。本文将详细介绍如何构建一个完整的微博爬虫和数据分析系统,从数据爬取、清洗、到多维度分析与可视…...
417. 太平洋大西洋水流问题
题目 有一个 m n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。 这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights…...
chili3d调试笔记8 打印零件属性
无效, 返回的是节点不是坐标啥的, 找他的属性 把document和selectednote(空集)传给handleshowproperty方法 怎么获得selectnotes和selectnotes的property值 有selectnotes运行这段就行了 明天再搞...