Files
APPBase/midiplayer/src/main/java/cc/winboll/studio/midiplayer/MidiParser.java

421 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:26
* @Describe MIDI文件解析器用于解析MIDI文件并提取轨道信息
*/
import cc.winboll.studio.libappbase.LogUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
public class MidiParser {
public static final String TAG = "MidiParser";
private static final Charset US_ASCII = Charset.forName("US-ASCII");
private InputStream mInputStream;
private int mTrackCount; // 轨道数量
private int mTicksPerBeat; // 每拍的ticks数从文件头解析
public MidiParser(File file) throws IOException {
this.mInputStream = new FileInputStream(file);
}
/**
* 解析MIDI文件返回包含轨道和每拍ticks数的结果支持速度控制
*/
public MidiPlayer.MidiParseResult parseWithTicks() throws IOException {
try {
// 1. 验证MIDI文件头MThd
if (!verifyHeader()) {
LogUtils.d(TAG, "不是有效的MIDI文件");
return null;
}
// 2. 读取文件头信息包含每拍ticks数
readHeaderInfo();
// 3. 解析每个轨道包含事件的deltaTicks
MidiPlayer.MidiTrack[] tracks = new MidiPlayer.MidiTrack[mTrackCount];
for (int i = 0; i < mTrackCount; i++) {
tracks[i] = parseTrackWithTicks();
}
// 返回解析结果(轨道数组 + 每拍ticks数
return new MidiPlayer.MidiParseResult(tracks, mTicksPerBeat);
} finally {
if (mInputStream != null) {
mInputStream.close();
}
}
}
/**
* 原始解析方法(兼容旧逻辑)
*/
public MidiTrack[] parse() throws IOException {
try {
if (!verifyHeader()) {
LogUtils.d(TAG, "不是有效的MIDI文件");
return null;
}
readHeaderInfo();
MidiTrack[] tracks = new MidiTrack[mTrackCount];
for (int i = 0; i < mTrackCount; i++) {
tracks[i] = parseTrack();
}
return tracks;
} finally {
if (mInputStream != null) {
mInputStream.close();
}
}
}
/**
* 验证MIDI文件头必须以"MThd"开头)
*/
private boolean verifyHeader() throws IOException {
byte[] header = new byte[4];
int read = mInputStream.read(header);
if (read != 4) {
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
return false;
}
String headerStr = new String(header, US_ASCII);
boolean isValid = "MThd".equals(headerStr);
if (!isValid) {
LogUtils.d(TAG, "无效的文件头标识: " + headerStr
+ " (十六进制: " + bytesToHex(header) + ")");
}
return isValid;
}
/**
* 读取MIDI文件头信息提取每拍ticks数
*/
private void readHeaderInfo() throws IOException {
// 1. 读取头长度4字节标准MIDI固定为6
int headerLength = readInt();
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
// 2. 读取头数据共6字节
byte[] headerData = new byte[6];
int read = mInputStream.read(headerData);
if (read != 6) {
LogUtils.d(TAG, "文件头数据不完整预期6字节实际读取: " + read);
throw new IOException("无效的MIDI文件头数据");
}
// 3. 解析头信息格式类型、轨道数、每拍ticks数
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 0xFF);
mTrackCount = ((headerData[2] & 0xFF) << 8) | (headerData[3] & 0xFF);
mTicksPerBeat = ((headerData[4] & 0xFF) << 8) | (headerData[5] & 0xFF); // 存储每拍ticks数
LogUtils.d(TAG, "MIDI文件格式: " + formatType);
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + mTicksPerBeat);
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
// 4. 处理扩展头
if (headerLength > 6) {
long skipped = mInputStream.skip(headerLength - 6);
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped);
}
}
/**
* 解析单个轨道包含事件的deltaTicks用于速度控制
*/
private MidiPlayer.MidiTrack parseTrackWithTicks() throws IOException {
// 1. 读取轨道头MTrk
byte[] trackHeader = new byte[4];
int headerRead = mInputStream.read(trackHeader);
if (headerRead != 4) {
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
}
// 2. 验证轨道头标识
String headerStr = new String(trackHeader, US_ASCII);
if (!"MTrk".equals(headerStr)) {
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr);
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
}
// 3. 读取轨道长度
int trackLength = readInt();
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
// 4. 读取完整轨道数据
byte[] trackData = new byte[trackLength];
int totalRead = 0;
while (totalRead < trackLength) {
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
if (bytesRead == -1) {
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead);
break;
}
totalRead += bytesRead;
}
// 5. 解析轨道事件包含deltaTicks
List<MidiPlayer.MidiEvent> events = new ArrayList<MidiPlayer.MidiEvent>();
if (totalRead == trackLength) {
parseEventsWithTicks(events, trackData);
} else {
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
}
return new MidiPlayer.MidiTrack(events);
}
/**
* 解析轨道事件提取deltaTicks用于计算播放延迟
*/
private void parseEventsWithTicks(List<MidiPlayer.MidiEvent> events, byte[] trackData) {
int offset = 0;
while (offset < trackData.length) {
// 1. 读取deltaTicks事件间隔单位ticks
long deltaTicks = readVariableLength(trackData, offset);
int deltaSize = getVariableLengthSize(deltaTicks);
offset += deltaSize;
if (offset >= trackData.length) {
break;
}
// 2. 读取事件状态字节
int statusByte = trackData[offset] & 0xFF;
offset++;
// 3. 确定事件数据长度
int dataLength = getEventDataLength(statusByte);
if (offset + dataLength > trackData.length) {
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte));
break;
}
// 4. 提取事件数据(状态字节+数据字节)
byte[] eventData = new byte[1 + dataLength];
eventData[0] = (byte) statusByte;
System.arraycopy(trackData, offset, eventData, 1, dataLength);
offset += dataLength;
// 5. 存储事件包含deltaTicks
events.add(new MidiPlayer.MidiEvent(eventData, (int) deltaTicks));
}
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + events.size());
}
/**
* 原始轨道解析方法(兼容旧逻辑)
*/
private MidiTrack parseTrack() throws IOException {
// 1. 读取轨道头MTrk
byte[] trackHeader = new byte[4];
int headerRead = mInputStream.read(trackHeader);
if (headerRead != 4) {
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
return new MidiTrack();
}
// 2. 验证轨道头标识
String headerStr = new String(trackHeader, US_ASCII);
if (!"MTrk".equals(headerStr)) {
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
return new MidiTrack();
}
// 3. 读取轨道长度
int trackLength = readInt();
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
// 4. 读取完整轨道数据
byte[] trackData = new byte[trackLength];
int totalRead = 0;
while (totalRead < trackLength) {
int bytesRead = mInputStream.read(trackData, totalRead, trackLength - totalRead);
if (bytesRead == -1) {
LogUtils.d(TAG, "轨道数据读取提前结束,已读取: " + totalRead + ",预期: " + trackLength);
break;
}
totalRead += bytesRead;
}
// 5. 解析轨道事件
MidiTrack track = new MidiTrack();
if (totalRead == trackLength) {
parseEvents(track, trackData);
} else {
LogUtils.d(TAG, "轨道数据不完整,跳过事件解析");
}
return track;
}
/**
* 原始事件解析方法(兼容旧逻辑)
*/
private void parseEvents(MidiTrack track, byte[] trackData) {
int offset = 0;
while (offset < trackData.length) {
// 1. 读取可变长度时间戳MIDI事件时间差
long deltaTime = readVariableLength(trackData, offset);
offset += getVariableLengthSize(deltaTime);
if (offset >= trackData.length) {
break;
}
// 2. 读取事件状态字节
int statusByte = trackData[offset] & 0xFF;
offset++;
// 3. 确定事件数据长度
int dataLength = getEventDataLength(statusByte);
if (offset + dataLength > trackData.length) {
LogUtils.d(TAG, "事件数据不完整,状态字节: 0x" + Integer.toHexString(statusByte)
+ ",剩余字节: " + (trackData.length - offset));
break;
}
// 4. 提取完整事件(状态字节+数据字节)
byte[] event = new byte[1 + dataLength];
event[0] = (byte) statusByte;
System.arraycopy(trackData, offset, event, 1, dataLength);
offset += dataLength;
track.addEvent(event);
}
LogUtils.d(TAG, "轨道事件解析完成,事件数量: " + track.getEventCount());
}
/**
* 根据状态字节获取事件数据长度
*/
private int getEventDataLength(int statusByte) {
int eventType = statusByte >> 4;
switch (eventType) {
case 0x8: // 音符关闭
case 0x9: // 音符开启
case 0xA: // 触后
case 0xB: // 控制器
case 0xE: // 弯音
return 2;
case 0xC: // 程序变更
case 0xD: // 通道触后
return 1;
case 0xF: // 系统事件
if (statusByte == 0xFF) { // 元事件
return 1;
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
return 0;
}
default:
return 0;
}
}
/**
* 读取可变长度整数MIDI事件时间差deltaTicks
*/
private long readVariableLength(byte[] data, int offset) {
long value = 0;
int b;
int i = 0;
do {
b = data[offset + i] & 0xFF;
value = (value << 7) | (b & 0x7F);
i++;
} while ((b & 0x80) != 0 && i < 4); // 最多4字节
return value;
}
/**
* 获取可变长度整数的字节数
*/
private int getVariableLengthSize(long value) {
if (value < 0x80) return 1;
if (value < 0x4000) return 2;
if (value < 0x200000) return 3;
return 4;
}
/**
* 读取无符号短整型2字节大端序
*/
private int readUnsignedShort() throws IOException {
int b1 = mInputStream.read() & 0xFF;
int b2 = mInputStream.read() & 0xFF;
return (b1 << 8) | b2;
}
/**
* 读取整型4字节大端序
*/
private int readInt() throws IOException {
int b1 = mInputStream.read() & 0xFF;
int b2 = mInputStream.read() & 0xFF;
int b3 = mInputStream.read() & 0xFF;
int b4 = mInputStream.read() & 0xFF;
return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
}
/**
* 字节数组转十六进制字符串(调试用)
*/
private String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
/**
* 旧轨道类(兼容旧逻辑)
*/
/*public static class MidiTrack {
private List<byte[]> events = new ArrayList<byte[]>();
private boolean isMuted = false;
private int currentIndex = 0;
public void addEvent(byte[] event) {
events.add(event);
}
public int getEventCount() {
return events.size();
}
public void setMute(boolean mute) {
isMuted = mute;
}
public boolean hasNextEvent() {
return currentIndex < events.size();
}
public byte[] nextEvent() {
if (currentIndex < events.size()) {
return events.get(currentIndex++);
}
return null;
}
public void reset() {
currentIndex = 0;
}
}*/
}