421 lines
14 KiB
Java
421 lines
14 KiB
Java
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;
|
||
}
|
||
}*/
|
||
}
|
||
|