修复MidiPlayer项目文件夹命名错误问题。
@@ -22,7 +22,7 @@ android {
|
|||||||
buildToolsVersion "32.0.0"
|
buildToolsVersion "32.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "cc.winboll.studio.miniplayer"
|
applicationId "cc.winboll.studio.midiplayer"
|
||||||
minSdkVersion 26
|
minSdkVersion 26
|
||||||
targetSdkVersion 30
|
targetSdkVersion 30
|
||||||
versionCode 1
|
versionCode 1
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sun Jun 29 04:03:38 GMT 2025
|
#Tue Sep 02 12:52:51 GMT 2025
|
||||||
stageCount=0
|
stageCount=1
|
||||||
libraryProject=
|
libraryProject=
|
||||||
baseVersion=15.0
|
baseVersion=15.0
|
||||||
publishVersion=15.0.0
|
publishVersion=15.0.0
|
||||||
buildCount=27
|
buildCount=2
|
||||||
baseBetaVersion=15.0.1
|
baseBetaVersion=15.0.1
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="cc.winboll.studio.miniplayer">
|
package="cc.winboll.studio.midiplayer">
|
||||||
|
|
||||||
<!-- 读取您共享存储空间中的内容 -->
|
<!-- 读取您共享存储空间中的内容 -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
@@ -23,20 +23,6 @@
|
|||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:name=".App">
|
android:name=".App">
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:label="@string/app_name">
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
|
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MidiPlayerActivity"
|
android:name=".MidiPlayerActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
BIN
midiplayer/src/main/assets/midi/SuperMarioBrothers.mid
Normal file
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
* @Date 2025/06/29 10:26
|
* @Date 2025/06/29 10:26
|
||||||
* @Describe MIDI文件解析器,用于解析MIDI文件并提取轨道信息
|
* @Describe MIDI文件解析器,用于解析MIDI文件并提取轨道信息
|
||||||
*/
|
*/
|
||||||
import android.util.Log;
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class MidiParser {
|
public class MidiParser {
|
||||||
public static final String TAG = "MidiParser";
|
public static final String TAG = "MidiParser";
|
||||||
// 替代StandardCharsets.US_ASCII,兼容低版本
|
|
||||||
private static final Charset US_ASCII = Charset.forName("US-ASCII");
|
private static final Charset US_ASCII = Charset.forName("US-ASCII");
|
||||||
|
|
||||||
private InputStream mInputStream;
|
private InputStream mInputStream;
|
||||||
private int mTrackCount; // 轨道数量
|
private int mTrackCount; // 轨道数量
|
||||||
|
private int mTicksPerBeat; // 每拍的ticks数(从文件头解析)
|
||||||
|
|
||||||
public MidiParser(File file) throws IOException {
|
public MidiParser(File file) throws IOException {
|
||||||
this.mInputStream = new FileInputStream(file);
|
this.mInputStream = new FileInputStream(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析MIDI文件,返回轨道数组
|
* 解析MIDI文件,返回包含轨道和每拍ticks数的结果(支持速度控制)
|
||||||
*/
|
*/
|
||||||
public MidiTrack[] parse() throws IOException {
|
public MidiPlayer.MidiParseResult parseWithTicks() throws IOException {
|
||||||
try {
|
try {
|
||||||
// 1. 验证MIDI文件头(MThd)
|
// 1. 验证MIDI文件头(MThd)
|
||||||
if (!verifyHeader()) {
|
if (!verifyHeader()) {
|
||||||
@@ -37,15 +37,39 @@ public class MidiParser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 读取文件头信息(包含轨道数量)
|
// 2. 读取文件头信息(包含每拍ticks数)
|
||||||
readHeaderInfo();
|
readHeaderInfo();
|
||||||
|
|
||||||
// 3. 解析每个轨道
|
// 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];
|
MidiTrack[] tracks = new MidiTrack[mTrackCount];
|
||||||
for (int i = 0; i < mTrackCount; i++) {
|
for (int i = 0; i < mTrackCount; i++) {
|
||||||
tracks[i] = parseTrack();
|
tracks[i] = parseTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
return tracks;
|
return tracks;
|
||||||
} finally {
|
} finally {
|
||||||
if (mInputStream != null) {
|
if (mInputStream != null) {
|
||||||
@@ -64,7 +88,6 @@ public class MidiParser {
|
|||||||
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
|
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// 使用US-ASCII编码验证(兼容低版本)
|
|
||||||
String headerStr = new String(header, US_ASCII);
|
String headerStr = new String(header, US_ASCII);
|
||||||
boolean isValid = "MThd".equals(headerStr);
|
boolean isValid = "MThd".equals(headerStr);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
@@ -75,14 +98,14 @@ public class MidiParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取MIDI文件头信息,修复文件指针偏移问题
|
* 读取MIDI文件头信息(提取每拍ticks数)
|
||||||
*/
|
*/
|
||||||
private void readHeaderInfo() throws IOException {
|
private void readHeaderInfo() throws IOException {
|
||||||
// 1. 读取头长度(4字节,标准MIDI固定为6)
|
// 1. 读取头长度(4字节,标准MIDI固定为6)
|
||||||
int headerLength = readInt();
|
int headerLength = readInt();
|
||||||
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
|
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
|
||||||
|
|
||||||
// 2. 读取头数据(共6字节:格式类型2字节 + 轨道数量2字节 + 时间分隔符2字节)
|
// 2. 读取头数据(共6字节)
|
||||||
byte[] headerData = new byte[6];
|
byte[] headerData = new byte[6];
|
||||||
int read = mInputStream.read(headerData);
|
int read = mInputStream.read(headerData);
|
||||||
if (read != 6) {
|
if (read != 6) {
|
||||||
@@ -90,45 +113,128 @@ public class MidiParser {
|
|||||||
throw new IOException("无效的MIDI文件头数据");
|
throw new IOException("无效的MIDI文件头数据");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 从headerData中解析信息(避免多次read导致指针偏移)
|
// 3. 解析头信息(格式类型、轨道数、每拍ticks数)
|
||||||
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 0xFF);
|
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 0xFF);
|
||||||
mTrackCount = ((headerData[2] & 0xFF) << 8) | (headerData[3] & 0xFF);
|
mTrackCount = ((headerData[2] & 0xFF) << 8) | (headerData[3] & 0xFF);
|
||||||
int ticksPerBeat = ((headerData[4] & 0xFF) << 8) | (headerData[5] & 0xFF);
|
mTicksPerBeat = ((headerData[4] & 0xFF) << 8) | (headerData[5] & 0xFF); // 存储每拍ticks数
|
||||||
|
|
||||||
LogUtils.d(TAG, "MIDI文件格式: " + formatType);
|
LogUtils.d(TAG, "MIDI文件格式: " + formatType);
|
||||||
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + ticksPerBeat);
|
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + mTicksPerBeat);
|
||||||
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
|
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
|
||||||
|
|
||||||
// 4. 处理扩展头(若头长度大于6,跳过剩余字节)
|
// 4. 处理扩展头
|
||||||
if (headerLength > 6) {
|
if (headerLength > 6) {
|
||||||
long skipped = mInputStream.skip(headerLength - 6);
|
long skipped = mInputStream.skip(headerLength - 6);
|
||||||
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped + " (预期: " + (headerLength - 6) + ")");
|
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析单个轨道信息
|
* 解析单个轨道(包含事件的deltaTicks,用于速度控制)
|
||||||
*/
|
*/
|
||||||
private MidiTrack parseTrack() throws IOException {
|
private MidiPlayer.MidiTrack parseTrackWithTicks() throws IOException {
|
||||||
MidiTrack track = new MidiTrack();
|
// 1. 读取轨道头(MTrk)
|
||||||
|
|
||||||
// 1. 读取轨道头(4字节)
|
|
||||||
byte[] trackHeader = new byte[4];
|
byte[] trackHeader = new byte[4];
|
||||||
int headerRead = mInputStream.read(trackHeader);
|
int headerRead = mInputStream.read(trackHeader);
|
||||||
if (headerRead != 4) {
|
if (headerRead != 4) {
|
||||||
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
|
LogUtils.d(TAG, "轨道头读取不完整,实际读取: " + headerRead + "字节");
|
||||||
return track;
|
return new MidiPlayer.MidiTrack(new ArrayList<MidiPlayer.MidiEvent>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 验证轨道头标识(MTrk),使用Tr-ASCII编码(兼容低版本)
|
// 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);
|
String headerStr = new String(trackHeader, US_ASCII);
|
||||||
if (!"MTrk".equals(headerStr)) {
|
if (!"MTrk".equals(headerStr)) {
|
||||||
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
|
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
|
||||||
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
|
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
|
||||||
return track;
|
return new MidiTrack();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 读取轨道长度(4字节)
|
// 3. 读取轨道长度
|
||||||
int trackLength = readInt();
|
int trackLength = readInt();
|
||||||
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
|
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
|
||||||
|
|
||||||
@@ -145,6 +251,7 @@ public class MidiParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 解析轨道事件
|
// 5. 解析轨道事件
|
||||||
|
MidiTrack track = new MidiTrack();
|
||||||
if (totalRead == trackLength) {
|
if (totalRead == trackLength) {
|
||||||
parseEvents(track, trackData);
|
parseEvents(track, trackData);
|
||||||
} else {
|
} else {
|
||||||
@@ -155,7 +262,7 @@ public class MidiParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析MIDI事件(按MIDI协议规范)
|
* 原始事件解析方法(兼容旧逻辑)
|
||||||
*/
|
*/
|
||||||
private void parseEvents(MidiTrack track, byte[] trackData) {
|
private void parseEvents(MidiTrack track, byte[] trackData) {
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
@@ -209,9 +316,9 @@ public class MidiParser {
|
|||||||
return 1;
|
return 1;
|
||||||
case 0xF: // 系统事件
|
case 0xF: // 系统事件
|
||||||
if (statusByte == 0xFF) { // 元事件
|
if (statusByte == 0xFF) { // 元事件
|
||||||
return 1; // 简化处理,实际需根据元事件类型判断
|
return 1;
|
||||||
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
|
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
|
||||||
return 0; // 复杂处理,暂不支持
|
return 0;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
@@ -219,7 +326,7 @@ public class MidiParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取可变长度整数(MIDI时间戳编码)
|
* 读取可变长度整数(MIDI事件时间差deltaTicks)
|
||||||
*/
|
*/
|
||||||
private long readVariableLength(byte[] data, int offset) {
|
private long readVariableLength(byte[] data, int offset) {
|
||||||
long value = 0;
|
long value = 0;
|
||||||
@@ -273,5 +380,41 @@ public class MidiParser {
|
|||||||
}
|
}
|
||||||
return sb.toString().trim();
|
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;
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,655 @@
|
|||||||
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
|
* @Date 2025/06/29 10:13
|
||||||
|
* @Describe MidiPlayer
|
||||||
|
*/
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.midi.MidiDevice;
|
||||||
|
import android.media.midi.MidiDeviceInfo;
|
||||||
|
import android.media.midi.MidiInputPort;
|
||||||
|
import android.media.midi.MidiManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI播放器核心类,支持加载MIDI文件、连接合成器、控制播放轨道及事件解析
|
||||||
|
* 需Android 6.0(API 23)及以上版本
|
||||||
|
*/
|
||||||
|
public class MidiPlayer {
|
||||||
|
|
||||||
|
public static final String TAG = "MidiPlayer";
|
||||||
|
|
||||||
|
// 上下文与系统服务
|
||||||
|
private final Context mContext;
|
||||||
|
private MidiManager mMidiManager;
|
||||||
|
|
||||||
|
// MIDI设备与端口
|
||||||
|
private MidiDevice mMidiDevice;
|
||||||
|
private MidiInputPort mInputPort;
|
||||||
|
private boolean isSynthConnected = false;
|
||||||
|
|
||||||
|
// 线程与Handler
|
||||||
|
private ExecutorService mExecutor;
|
||||||
|
private final Handler mHandler = new Handler();
|
||||||
|
|
||||||
|
// 播放数据与状态
|
||||||
|
private MidiTrack[] mTracks;
|
||||||
|
private boolean isPlaying = false;
|
||||||
|
private int mCurrentTrack = -1; // -1表示播放所有轨道
|
||||||
|
private int mTicksPerBeat; // 每拍的ticks数(从MIDI文件解析)
|
||||||
|
private long startTime; // 播放开始时间(用于日志)
|
||||||
|
|
||||||
|
// 音色库管理
|
||||||
|
private final SoundFontManager mSoundFontManager;
|
||||||
|
|
||||||
|
// 连接回调接口
|
||||||
|
public interface OnSynthConnectedListener {
|
||||||
|
void onConnected(boolean success);
|
||||||
|
}
|
||||||
|
private OnSynthConnectedListener mConnectionListener;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI事件类,存储事件数据及时间间隔
|
||||||
|
*/
|
||||||
|
public static class MidiEvent {
|
||||||
|
public byte[] data; // 事件指令数据
|
||||||
|
public int deltaTicks; // 与上一事件的时间间隔(ticks)
|
||||||
|
|
||||||
|
public MidiEvent(byte[] data, int deltaTicks) {
|
||||||
|
this.data = data;
|
||||||
|
this.deltaTicks = deltaTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI轨道类,包含事件列表及播放状态
|
||||||
|
*/
|
||||||
|
public static class MidiTrack {
|
||||||
|
private final List<MidiEvent> events;
|
||||||
|
private boolean isMuted = false;
|
||||||
|
private int currentIndex = 0;
|
||||||
|
|
||||||
|
public MidiTrack(List<MidiEvent> events) {
|
||||||
|
this.events = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNextEvent() {
|
||||||
|
return currentIndex < events.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MidiEvent nextEvent() {
|
||||||
|
return currentIndex < events.size() ? events.get(currentIndex++) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMute(boolean mute) {
|
||||||
|
isMuted = mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMuted() {
|
||||||
|
return isMuted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI解析结果类,包含轨道数组与每拍ticks数
|
||||||
|
*/
|
||||||
|
public static class MidiParseResult {
|
||||||
|
public MidiTrack[] tracks;
|
||||||
|
public int ticksPerBeat;
|
||||||
|
|
||||||
|
public MidiParseResult(MidiTrack[] tracks, int ticksPerBeat) {
|
||||||
|
this.tracks = tracks;
|
||||||
|
this.ticksPerBeat = ticksPerBeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 构造方法
|
||||||
|
public MidiPlayer(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mSoundFontManager = new SoundFontManager(context);
|
||||||
|
// 初始化MIDI服务(需API 23+)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
mMidiManager = (MidiManager) context.getSystemService(Context.MIDI_SERVICE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 设置合成器连接回调
|
||||||
|
public void setOnSynthConnectedListener(OnSynthConnectedListener listener) {
|
||||||
|
mConnectionListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载MIDI文件并解析为轨道
|
||||||
|
* @param file MIDI文件
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
public boolean loadMidiFile(File file) {
|
||||||
|
try {
|
||||||
|
MidiParser parser = new MidiParser(file);
|
||||||
|
MidiParseResult result = parser.parseWithTicks();
|
||||||
|
mTracks = result.tracks;
|
||||||
|
mTicksPerBeat = result.ticksPerBeat;
|
||||||
|
return mTracks != null && mTracks.length > 0;
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.d(TAG, "加载MIDI文件失败: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载音色库文件
|
||||||
|
* @param soundFontFile 音色库文件
|
||||||
|
* @return 是否加载成功
|
||||||
|
*/
|
||||||
|
public boolean loadSoundFont(File soundFontFile) {
|
||||||
|
return mSoundFontManager.loadSoundFont(soundFontFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始播放MIDI文件
|
||||||
|
*/
|
||||||
|
public void start() {
|
||||||
|
LogUtils.d(TAG, "start()");
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
LogUtils.d(TAG, "需Android 6.0及以上版本");
|
||||||
|
notifyConnectionResult(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPlaying || mTracks == null || mMidiManager == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaying = true;
|
||||||
|
getExecutor().execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
LogUtils.d(TAG, "开始连接合成器");
|
||||||
|
isSynthConnected = false;
|
||||||
|
connectToSynth();
|
||||||
|
|
||||||
|
// 等待合成器连接(最多3秒)
|
||||||
|
int waitCount = 0;
|
||||||
|
while (!isSynthConnected && waitCount < 30) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
waitCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSynthConnected || mInputPort == null) {
|
||||||
|
LogUtils.d(TAG, "合成器连接失败");
|
||||||
|
isPlaying = false;
|
||||||
|
notifyConnectionResult(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyConnectionResult(true);
|
||||||
|
playTracksWithSpeedControl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带速度控制的轨道播放逻辑
|
||||||
|
*/
|
||||||
|
private void playTracksWithSpeedControl() {
|
||||||
|
LogUtils.d(TAG, "开始播放,每拍ticks数: " + mTicksPerBeat);
|
||||||
|
if (mInputPort == null || mTracks == null || mTicksPerBeat <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
int trackCount = mTracks.length;
|
||||||
|
long[] trackNextEventTime = new long[trackCount]; // 各轨道下一事件的播放时间(ms)
|
||||||
|
|
||||||
|
// 初始化轨道状态
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
mTracks[i].reset();
|
||||||
|
trackNextEventTime[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认BPM(可通过MIDI文件中的tempo事件动态调整)
|
||||||
|
int bpm = 120;
|
||||||
|
double msPerTick = (60.0 / bpm) * 1000 / mTicksPerBeat;
|
||||||
|
|
||||||
|
// 循环播放事件
|
||||||
|
while (isPlaying) {
|
||||||
|
// 找到最早需要播放的事件时间
|
||||||
|
long earliestTime = Long.MAX_VALUE;
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
if ((mCurrentTrack == -1 || mCurrentTrack == i)
|
||||||
|
&& !mTracks[i].isMuted()
|
||||||
|
&& mTracks[i].hasNextEvent()) {
|
||||||
|
earliestTime = Math.min(earliestTime, trackNextEventTime[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (earliestTime == Long.MAX_VALUE) {
|
||||||
|
LogUtils.d(TAG, "所有轨道事件播放完毕");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算等待时间并休眠
|
||||||
|
long currentTime = System.currentTimeMillis() - startTime;
|
||||||
|
long delay = earliestTime - currentTime;
|
||||||
|
if (delay > 0) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(delay);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放所有到达时间点的事件
|
||||||
|
for (int i = 0; i < trackCount; i++) {
|
||||||
|
if ((mCurrentTrack == -1 || mCurrentTrack == i)
|
||||||
|
&& !mTracks[i].isMuted()
|
||||||
|
&& mTracks[i].hasNextEvent()
|
||||||
|
&& trackNextEventTime[i] == earliestTime) {
|
||||||
|
|
||||||
|
MidiEvent event = mTracks[i].nextEvent();
|
||||||
|
if (event != null) {
|
||||||
|
try {
|
||||||
|
logMidiEventDetails(i, event);
|
||||||
|
mInputPort.send(event.data, 0, event.data.length);
|
||||||
|
// 更新下一事件时间
|
||||||
|
trackNextEventTime[i] = earliestTime + (long) (event.deltaTicks * msPerTick);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.d(TAG, "发送事件失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaying = false;
|
||||||
|
LogUtils.d(TAG, "播放结束");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析MIDI事件并输出详细日志
|
||||||
|
*/
|
||||||
|
private void logMidiEventDetails(int trackIndex, MidiEvent event) {
|
||||||
|
// 无效事件增加原始数据日志,便于调试
|
||||||
|
if (event.data == null || event.data.length < 2) {
|
||||||
|
String dataStr = (event.data != null) ? Arrays.toString(event.data) : "null";
|
||||||
|
LogUtils.d(TAG, "轨道[" + trackIndex + "] 无效事件: 数据长度不足 ("
|
||||||
|
+ (event.data != null ? event.data.length : 0) + "字节),原始数据: " + dataStr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int statusByte = event.data[0] & 0xFF;
|
||||||
|
int eventType = statusByte & 0xF0;
|
||||||
|
int channel = (statusByte & 0x0F) + 1; // 通道号1-16
|
||||||
|
|
||||||
|
StringBuilder log = new StringBuilder();
|
||||||
|
log.append("轨道[").append(trackIndex).append("] 事件类型: ");
|
||||||
|
|
||||||
|
switch (eventType) {
|
||||||
|
case 0x90: // 音符开启
|
||||||
|
if (event.data.length >= 3) {
|
||||||
|
int pitch = event.data[1] & 0xFF;
|
||||||
|
int velocity = event.data[2] & 0xFF;
|
||||||
|
log.append("音符开启 | 通道: ").append(channel)
|
||||||
|
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
|
||||||
|
.append(" | 力度: ").append(velocity)
|
||||||
|
.append(" | 间隔ticks: ").append(event.deltaTicks)
|
||||||
|
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
|
||||||
|
} else {
|
||||||
|
log.append("音符开启 (数据不完整) | 长度: ").append(event.data.length);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0x80: // 音符关闭
|
||||||
|
if (event.data.length >= 3) {
|
||||||
|
int pitch = event.data[1] & 0xFF;
|
||||||
|
int velocity = event.data[2] & 0xFF;
|
||||||
|
log.append("音符关闭 | 通道: ").append(channel)
|
||||||
|
.append(" | 音高: ").append(pitch).append(" (").append(getNoteName(pitch)).append(")")
|
||||||
|
.append(" | 力度: ").append(velocity)
|
||||||
|
.append(" | 间隔ticks: ").append(event.deltaTicks)
|
||||||
|
.append(" | 播放时间: ").append(System.currentTimeMillis() - startTime).append("ms");
|
||||||
|
} else {
|
||||||
|
log.append("音符关闭 (数据不完整) | 长度: ").append(event.data.length);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xB0: // 控制变化
|
||||||
|
if (event.data.length >= 3) {
|
||||||
|
int controlNumber = event.data[1] & 0xFF;
|
||||||
|
int controlValue = event.data[2] & 0xFF;
|
||||||
|
log.append("控制变化 | 通道: ").append(channel)
|
||||||
|
.append(" | 控制器: ").append(controlNumber).append(" (").append(getControlName(controlNumber)).append(")")
|
||||||
|
.append(" | 数值: ").append(controlValue);
|
||||||
|
} else {
|
||||||
|
log.append("控制变化 (数据不完整) | 长度: ").append(event.data.length);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xC0: // 程序改变(乐器切换)
|
||||||
|
if (event.data.length >= 2) {
|
||||||
|
int program = event.data[1] & 0xFF;
|
||||||
|
// 增加乐器编号有效性校验日志
|
||||||
|
String instrumentName = getInstrumentName(program);
|
||||||
|
if (program < 0 || program >= 128) {
|
||||||
|
instrumentName += " (超出GM标准范围0-127)";
|
||||||
|
}
|
||||||
|
log.append("程序改变 | 通道: ").append(channel)
|
||||||
|
.append(" | 乐器编号: ").append(program).append(" (").append(instrumentName).append(")");
|
||||||
|
} else {
|
||||||
|
log.append("程序改变 (数据不完整) | 长度: ").append(event.data.length);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xD0: // 通道触后事件
|
||||||
|
log.append("通道触后 | 通道: ").append(channel)
|
||||||
|
.append(" | 压力值: ").append(event.data[1] & 0xFF);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 0xF0: // 系统专属事件(SysEx)
|
||||||
|
log.append("系统专属事件(SysEx) | 长度: ").append(event.data.length)
|
||||||
|
.append(" | 首字节: 0x").append(Integer.toHexString(event.data[0] & 0xFF));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.append("未知类型 (0x").append(Integer.toHexString(eventType)).append(") | 通道: ").append(channel)
|
||||||
|
.append(",原始数据: ").append(Arrays.toString(event.data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogUtils.d(TAG, log.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 音高转音符名称(如60→C4)
|
||||||
|
*/
|
||||||
|
private String getNoteName(int pitch) {
|
||||||
|
if (pitch < 0 || pitch > 127) return "无效音高";
|
||||||
|
String[] noteNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
|
||||||
|
int octave = (pitch / 12) - 1; // C4对应60
|
||||||
|
return noteNames[pitch % 12] + octave;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIDI控制器编号转名称
|
||||||
|
*/
|
||||||
|
private String getControlName(int controlNumber) {
|
||||||
|
switch (controlNumber) {
|
||||||
|
case 0: return "Bank Select (MSB)";
|
||||||
|
case 1: return "Modulation Wheel";
|
||||||
|
case 7: return "音量控制";
|
||||||
|
case 10: return "声像控制";
|
||||||
|
case 11: return "表达控制";
|
||||||
|
case 64: return "延音踏板";
|
||||||
|
case 121: return "重置所有控制器";
|
||||||
|
default: return "未知控制器";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 乐器编号转名称(GM标准)
|
||||||
|
*/
|
||||||
|
private String getInstrumentName(int program) {
|
||||||
|
String[] instruments = {
|
||||||
|
"钢琴", "明亮钢琴", "电钢琴", "Honky-tonk钢琴", "电钢琴", "羽管键琴",
|
||||||
|
"击弦古钢琴", "颤音琴", "竖琴", "管风琴", "手风琴", "acoustic贝斯",
|
||||||
|
"电贝斯(指弹)", "电贝斯(拨片)", "小提琴", "大提琴"
|
||||||
|
};
|
||||||
|
if (program < 0 || program >= 128) {
|
||||||
|
return "无效编号";
|
||||||
|
}
|
||||||
|
if (program < instruments.length) {
|
||||||
|
return instruments[program];
|
||||||
|
} else {
|
||||||
|
return "乐器 " + program;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接MIDI合成器
|
||||||
|
*/
|
||||||
|
public void connectToSynth() {
|
||||||
|
if (mMidiManager == null) return;
|
||||||
|
closeMidiResources();
|
||||||
|
searchSynthInDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索并选择可用的MIDI设备
|
||||||
|
*/
|
||||||
|
private void searchSynthInDevices() {
|
||||||
|
MidiDeviceInfo[] devices = mMidiManager.getDevices();
|
||||||
|
if (devices == null || devices.length == 0) {
|
||||||
|
LogUtils.d(TAG, "未检测到MIDI设备");
|
||||||
|
isSynthConnected = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先选择合成器类型(3),其次选择有输入端口的设备
|
||||||
|
MidiDeviceInfo targetDevice = null;
|
||||||
|
for (MidiDeviceInfo device : devices) {
|
||||||
|
if (device.getType() == 3) {
|
||||||
|
targetDevice = device;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (targetDevice == null && device.getInputPortCount() > 0) {
|
||||||
|
targetDevice = device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetDevice != null) {
|
||||||
|
LogUtils.d(TAG, "尝试打开设备: 设备ID=" + targetDevice.getId() + ",名称=" + targetDevice.getProperties().getString(MidiDeviceInfo.PROPERTY_NAME));
|
||||||
|
mMidiManager.openDevice(targetDevice, new MidiManager.OnDeviceOpenedListener() {
|
||||||
|
@Override
|
||||||
|
public void onDeviceOpened(MidiDevice device) {
|
||||||
|
setupMidiDevice(device);
|
||||||
|
}
|
||||||
|
}, mHandler);
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "未找到可用设备(无合成器或带输入端口的设备)");
|
||||||
|
isSynthConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化MIDI设备并打开输入端口
|
||||||
|
*/
|
||||||
|
private void setupMidiDevice(MidiDevice device) {
|
||||||
|
if (device == null) {
|
||||||
|
LogUtils.d(TAG, "打开MIDI设备失败: 设备为null");
|
||||||
|
isSynthConnected = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mMidiDevice = device;
|
||||||
|
try {
|
||||||
|
// 从设备信息中获取输入端口数量
|
||||||
|
MidiDeviceInfo deviceInfo = device.getInfo();
|
||||||
|
if (deviceInfo.getInputPortCount() > 0) {
|
||||||
|
mInputPort = device.openInputPort(0);
|
||||||
|
isSynthConnected = (mInputPort != null);
|
||||||
|
LogUtils.d(TAG, isSynthConnected ? "成功打开输入端口(端口0)" : "输入端口为null,打开失败");
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "设备无输入端口,无法接收MIDI事件");
|
||||||
|
isSynthConnected = false;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtils.d(TAG, "打开端口失败: " + e.getMessage());
|
||||||
|
isSynthConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭MIDI设备及端口资源
|
||||||
|
*/
|
||||||
|
private void closeMidiResources() {
|
||||||
|
if (mInputPort != null) {
|
||||||
|
try {
|
||||||
|
mInputPort.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.d(TAG, "关闭输入端口失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
mInputPort = null;
|
||||||
|
}
|
||||||
|
if (mMidiDevice != null) {
|
||||||
|
try {
|
||||||
|
mMidiDevice.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtils.d(TAG, "关闭MIDI设备失败: " + e.getMessage());
|
||||||
|
}
|
||||||
|
mMidiDevice = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知合成器连接结果
|
||||||
|
*/
|
||||||
|
private void notifyConnectionResult(final boolean success) {
|
||||||
|
if (mConnectionListener != null) {
|
||||||
|
mHandler.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
mConnectionListener.onConnected(success);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置指定轨道静音状态
|
||||||
|
* @param trackIndex 轨道索引
|
||||||
|
* @param mute 是否静音
|
||||||
|
*/
|
||||||
|
public void setTrackMute(int trackIndex, boolean mute) {
|
||||||
|
if (mTracks == null) {
|
||||||
|
LogUtils.d(TAG, "设置静音失败:未加载MIDI轨道");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trackIndex >= 0 && trackIndex < mTracks.length) {
|
||||||
|
mTracks[trackIndex].setMute(mute);
|
||||||
|
LogUtils.d(TAG, "轨道[" + trackIndex + "] 静音状态: " + (mute ? "已静音" : "正常播放"));
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "设置静音失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换当前播放轨道(-1表示所有轨道)
|
||||||
|
* @param trackIndex 轨道索引
|
||||||
|
*/
|
||||||
|
public void setCurrentTrack(int trackIndex) {
|
||||||
|
if (mTracks == null) {
|
||||||
|
LogUtils.d(TAG, "切换轨道失败:未加载MIDI轨道");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 校验轨道索引有效性
|
||||||
|
if (trackIndex == -1 || (trackIndex >= 0 && trackIndex < mTracks.length)) {
|
||||||
|
mCurrentTrack = trackIndex;
|
||||||
|
LogUtils.d(TAG, "切换当前播放轨道: " + (trackIndex == -1 ? "所有轨道" : "轨道[" + trackIndex + "]"));
|
||||||
|
} else {
|
||||||
|
LogUtils.d(TAG, "切换轨道失败:无效轨道索引 " + trackIndex + "(总轨道数:" + mTracks.length + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取轨道总数
|
||||||
|
* @return 轨道数量
|
||||||
|
*/
|
||||||
|
public int getTrackCount() {
|
||||||
|
return mTracks != null ? mTracks.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 暂停播放
|
||||||
|
*/
|
||||||
|
public void pause() {
|
||||||
|
boolean wasPlaying = isPlaying;
|
||||||
|
isPlaying = false;
|
||||||
|
LogUtils.d(TAG, "播放暂停: " + (wasPlaying ? "已暂停当前播放" : "当前未在播放"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止播放并释放所有资源
|
||||||
|
*/
|
||||||
|
public void stop() {
|
||||||
|
boolean wasPlaying = isPlaying;
|
||||||
|
isPlaying = false;
|
||||||
|
isSynthConnected = false;
|
||||||
|
closeMidiResources();
|
||||||
|
if (mExecutor != null) {
|
||||||
|
mExecutor.shutdownNow();
|
||||||
|
mExecutor = null;
|
||||||
|
}
|
||||||
|
LogUtils.d(TAG, "播放停止: " + (wasPlaying ? "已终止播放并释放资源" : "当前未在播放"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取线程池(单例)
|
||||||
|
*/
|
||||||
|
private ExecutorService getExecutor() {
|
||||||
|
if (mExecutor == null || mExecutor.isShutdown() || mExecutor.isTerminated()) {
|
||||||
|
mExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
}
|
||||||
|
return mExecutor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查合成器是否已连接
|
||||||
|
* @return 连接状态
|
||||||
|
*/
|
||||||
|
public boolean isSynthConnected() {
|
||||||
|
return isSynthConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前MIDI输入端口
|
||||||
|
* @return 输入端口
|
||||||
|
*/
|
||||||
|
public MidiInputPort getInputPort() {
|
||||||
|
return mInputPort;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen&豆包大模型<zhangsken@188.com>
|
||||||
* @Date 2025/06/29 10:10
|
* @Date 2025/06/29 10:10
|
||||||
* @Describe Midi 播放窗口
|
* @Describe Midi 播放窗口
|
||||||
*/
|
*/
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.media.midi.MidiInputPort;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -17,6 +18,7 @@ import android.widget.Button;
|
|||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import com.hjq.toast.ToastUtils;
|
import com.hjq.toast.ToastUtils;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -27,12 +29,12 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
public static final String TAG = "MidiPlayerActivity";
|
public static final String TAG = "MidiPlayerActivity";
|
||||||
|
|
||||||
private MidiPlayer mMidiPlayer;
|
private MidiPlayer mMidiPlayer;
|
||||||
private Button mPlayBtn, mPauseBtn, mStopBtn;
|
private Button mPlayBtn, mPauseBtn, mStopBtn, mTestBtn;
|
||||||
private ListView mTrackListView;
|
private ListView mTrackListView;
|
||||||
private ListView mFileListView; // 文件列表
|
private ListView mFileListView;
|
||||||
private TrackAdapter mTrackAdapter;
|
private TrackAdapter mTrackAdapter;
|
||||||
private TextView mFileNameTv;
|
private TextView mFileNameTv;
|
||||||
private List<File> mMidiFileList = new ArrayList<>(); // 存储midi文件列表
|
private List<File> mMidiFileList = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Activity getActivity() {
|
public Activity getActivity() {
|
||||||
@@ -47,31 +49,194 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_midi_player); // 需确保布局文件存在
|
setContentView(R.layout.activity_midi_player);
|
||||||
|
|
||||||
// 初始化控件
|
// 初始化控件
|
||||||
mPlayBtn = (Button) findViewById(R.id.btn_play);
|
mPlayBtn = (Button) findViewById(R.id.btn_play);
|
||||||
mPauseBtn = (Button) findViewById(R.id.btn_pause);
|
mPauseBtn = (Button) findViewById(R.id.btn_pause);
|
||||||
mStopBtn = (Button) findViewById(R.id.btn_stop);
|
mStopBtn = (Button) findViewById(R.id.btn_stop);
|
||||||
|
mTestBtn = (Button) findViewById(R.id.btn_test);
|
||||||
mTrackListView = (ListView) findViewById(R.id.lv_tracks);
|
mTrackListView = (ListView) findViewById(R.id.lv_tracks);
|
||||||
mFileListView = (ListView) findViewById(R.id.lv_midi_files);
|
mFileListView = (ListView) findViewById(R.id.lv_midi_files);
|
||||||
mFileNameTv = (TextView) findViewById(R.id.tv_file_name);
|
mFileNameTv = (TextView) findViewById(R.id.tv_file_name);
|
||||||
|
|
||||||
// 检查系统版本兼容性
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
initMidiPlayer();
|
initMidiPlayer();
|
||||||
loadMidiFileList(); // 加载本地MIDI文件
|
loadMidiFileList();
|
||||||
initControlButtons();
|
initControlButtons();
|
||||||
|
initTestButton();
|
||||||
} else {
|
} else {
|
||||||
mFileNameTv.setText("当前设备不支持MIDI播放(需Android 6.0+)");
|
mFileNameTv.setText("当前设备不支持MIDI播放(需Android 6.0及以上)");
|
||||||
disableButtons();
|
disableButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copyAssetsMidiFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化MIDI播放器并设置连接回调
|
// 初始化测试按钮及1分钟测试逻辑
|
||||||
|
private void initTestButton() {
|
||||||
|
mTestBtn.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
testMidiOutput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1分钟MIDI测试序列:包含多样音符组合,验证输出功能
|
||||||
|
*/
|
||||||
|
private void testMidiOutput() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||||
|
ToastUtils.show("测试需要Android 6.0及以上");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mMidiPlayer == null) {
|
||||||
|
initMidiPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// 确保合成器连接
|
||||||
|
if (!mMidiPlayer.isSynthConnected()) {
|
||||||
|
LogUtils.d(TAG, "测试:连接合成器中...");
|
||||||
|
mMidiPlayer.connectToSynth();
|
||||||
|
|
||||||
|
// 最多等待3秒连接
|
||||||
|
int waitCount = 0;
|
||||||
|
while (!mMidiPlayer.isSynthConnected() && waitCount < 30) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
waitCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final MidiInputPort inputPort = mMidiPlayer.getInputPort();
|
||||||
|
if (inputPort == null) {
|
||||||
|
LogUtils.d(TAG, "测试失败:无可用输入端口");
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("测试失败:未找到输入端口");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("开始1分钟测试音符...");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 测试序列定义:[音高, 力度, 时长(毫秒)]
|
||||||
|
int[][] noteSequence = {
|
||||||
|
// 0-10秒:低音区短音符
|
||||||
|
{48, 64, 200}, {50, 64, 200}, {52, 64, 200}, {53, 64, 200},
|
||||||
|
{55, 64, 200}, {57, 64, 200}, {59, 64, 200}, {60, 64, 400},
|
||||||
|
|
||||||
|
// 10-20秒:中音区连音
|
||||||
|
{60, 72, 300}, {62, 72, 300}, {64, 72, 300}, {65, 72, 300},
|
||||||
|
{67, 72, 300}, {69, 72, 300}, {71, 72, 300}, {72, 72, 600},
|
||||||
|
|
||||||
|
// 20-30秒:高音区跳音
|
||||||
|
{72, 80, 150}, {76, 80, 150}, {79, 80, 150}, {84, 80, 300},
|
||||||
|
{79, 80, 150}, {76, 80, 150}, {72, 80, 150}, {69, 80, 450},
|
||||||
|
|
||||||
|
// 30-40秒:和弦组合
|
||||||
|
{60, 64, 400}, {64, 64, 400}, {67, 64, 400}, // C和弦
|
||||||
|
{62, 64, 400}, {65, 64, 400}, {69, 64, 400}, // Dm和弦
|
||||||
|
{64, 64, 400}, {67, 64, 400}, {71, 64, 400}, // Em和弦
|
||||||
|
|
||||||
|
// 40-50秒:渐强长音
|
||||||
|
{55, 40, 1000}, {55, 60, 1000}, {55, 80, 1000}, {55, 100, 1000},
|
||||||
|
|
||||||
|
// 50-60秒:快速音阶
|
||||||
|
{50, 70, 100}, {52, 70, 100}, {53, 70, 100}, {55, 70, 100},
|
||||||
|
{57, 70, 100}, {59, 70, 100}, {60, 70, 100}, {62, 70, 100},
|
||||||
|
{64, 70, 100}, {65, 70, 100}, {67, 70, 100}, {69, 70, 100},
|
||||||
|
{71, 70, 100}, {72, 90, 500}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送测试序列
|
||||||
|
for (int[] note : noteSequence) {
|
||||||
|
if (!mMidiPlayer.isSynthConnected()) {
|
||||||
|
LogUtils.d(TAG, "连接已断开,停止测试");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pitch = note[0];
|
||||||
|
int velocity = note[1];
|
||||||
|
int duration = note[2];
|
||||||
|
|
||||||
|
// 发送音符开启事件
|
||||||
|
byte[] noteOn = new byte[]{(byte) 0x90, (byte) pitch, (byte) velocity};
|
||||||
|
inputPort.send(noteOn, 0, noteOn.length);
|
||||||
|
//LogUtils.d(TAG, "测试音符:音高=" + pitch + ", 力度=" + velocity + ", 时长=" + duration + "ms");
|
||||||
|
|
||||||
|
// 等待音符时长
|
||||||
|
Thread.sleep(duration);
|
||||||
|
|
||||||
|
// 发送音符关闭事件
|
||||||
|
byte[] noteOff = new byte[]{(byte) 0x80, (byte) pitch, 0};
|
||||||
|
inputPort.send(noteOff, 0, noteOff.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("1分钟测试完成");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (final Exception e) {
|
||||||
|
LogUtils.d(TAG, "测试出错:" + e.getMessage());
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ToastUtils.show("测试出错:" + e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拷贝Assets中的MIDI文件到本地
|
||||||
|
private void copyAssetsMidiFiles() {
|
||||||
|
final AssetMidiCopier copier = new AssetMidiCopier(this);
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
final boolean success = copier.copyMidiFiles();
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (success) {
|
||||||
|
File midiDir = copier.getMidiTargetDir();
|
||||||
|
LogUtils.d(TAG, "MIDI文件拷贝成功,路径:" + midiDir.getAbsolutePath());
|
||||||
|
// 重新加载文件列表
|
||||||
|
loadMidiFileList();
|
||||||
|
} else {
|
||||||
|
ToastUtils.show("MIDI文件拷贝失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化MIDI播放器
|
||||||
private void initMidiPlayer() {
|
private void initMidiPlayer() {
|
||||||
mMidiPlayer = new MidiPlayer(this);
|
mMidiPlayer = new MidiPlayer(this);
|
||||||
// 设置合成器连接状态回调(Java 7使用匿名内部类替代Lambda)
|
|
||||||
mMidiPlayer.setOnSynthConnectedListener(new MidiPlayer.OnSynthConnectedListener() {
|
mMidiPlayer.setOnSynthConnectedListener(new MidiPlayer.OnSynthConnectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onConnected(final boolean success) {
|
public void onConnected(final boolean success) {
|
||||||
@@ -92,14 +257,13 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
// 加载本地MIDI文件列表
|
// 加载本地MIDI文件列表
|
||||||
private void loadMidiFileList() {
|
private void loadMidiFileList() {
|
||||||
File midiDir = new File(getFilesDir(), "midi");
|
File midiDir = new File(getFilesDir(), "midi");
|
||||||
// 检查目录是否存在
|
|
||||||
if (!midiDir.exists()) {
|
if (!midiDir.exists()) {
|
||||||
midiDir.mkdirs(); // 创建目录
|
midiDir.mkdirs();
|
||||||
mFileNameTv.setText("midi目录已创建,请放入文件");
|
mFileNameTv.setText("midi目录已创建,等待文件拷贝...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 筛选.mid和.midi文件
|
mMidiFileList.clear();
|
||||||
File[] files = midiDir.listFiles();
|
File[] files = midiDir.listFiles();
|
||||||
if (files != null) {
|
if (files != null) {
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
@@ -110,39 +274,37 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示文件列表
|
|
||||||
if (mMidiFileList.isEmpty()) {
|
if (mMidiFileList.isEmpty()) {
|
||||||
mFileNameTv.setText("midi目录中无可用文件");
|
mFileNameTv.setText("midi目录中无可用文件");
|
||||||
} else {
|
} else {
|
||||||
showFileList();
|
showFileList();
|
||||||
|
mFileNameTv.setText("找到" + mMidiFileList.size() + "个MIDI文件");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示文件列表到ListView
|
// 显示MIDI文件列表
|
||||||
private void showFileList() {
|
private void showFileList() {
|
||||||
List<String> fileNameList = new ArrayList<>();
|
List<String> fileNameList = new ArrayList<>();
|
||||||
for (File file : mMidiFileList) {
|
for (File file : mMidiFileList) {
|
||||||
fileNameList.add(file.getName());
|
fileNameList.add(file.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置文件列表适配器
|
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
|
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
|
||||||
this,
|
this,
|
||||||
android.R.layout.simple_list_item_1,
|
android.R.layout.simple_list_item_1,
|
||||||
fileNameList
|
fileNameList
|
||||||
);
|
);
|
||||||
mFileListView.setAdapter(adapter);
|
mFileListView.setAdapter(adapter);
|
||||||
|
|
||||||
// 文件点击事件:加载选中的MIDI文件(Java 7使用匿名内部类替代Lambda)
|
|
||||||
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
mMidiPlayer.stop(); // 停止当前播放
|
mMidiPlayer.stop();
|
||||||
File selectedFile = mMidiFileList.get(position);
|
File selectedFile = mMidiFileList.get(position);
|
||||||
boolean loaded = mMidiPlayer.loadMidiFile(selectedFile);
|
boolean loaded = mMidiPlayer.loadMidiFile(selectedFile);
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
mFileNameTv.setText("当前文件:" + selectedFile.getName());
|
mFileNameTv.setText("当前文件:" + selectedFile.getName());
|
||||||
initTrackList(); // 刷新轨道列表
|
initTrackList();
|
||||||
ToastUtils.show("已加载:" + selectedFile.getName());
|
ToastUtils.show("已加载:" + selectedFile.getName());
|
||||||
} else {
|
} else {
|
||||||
mFileNameTv.setText("文件加载失败:" + selectedFile.getName());
|
mFileNameTv.setText("文件加载失败:" + selectedFile.getName());
|
||||||
@@ -151,7 +313,7 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化轨道列表(显示所有轨道的控制项)
|
// 初始化轨道列表
|
||||||
private void initTrackList() {
|
private void initTrackList() {
|
||||||
mTrackAdapter = new TrackAdapter(this, mMidiPlayer);
|
mTrackAdapter = new TrackAdapter(this, mMidiPlayer);
|
||||||
mTrackListView.setAdapter(mTrackAdapter);
|
mTrackListView.setAdapter(mTrackAdapter);
|
||||||
@@ -187,14 +349,15 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁用所有控制按钮
|
// 禁用所有按钮(低版本设备)
|
||||||
private void disableButtons() {
|
private void disableButtons() {
|
||||||
mPlayBtn.setEnabled(false);
|
mPlayBtn.setEnabled(false);
|
||||||
mPauseBtn.setEnabled(false);
|
mPauseBtn.setEnabled(false);
|
||||||
mStopBtn.setEnabled(false);
|
mStopBtn.setEnabled(false);
|
||||||
|
mTestBtn.setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 轨道控制适配器(每个轨道显示静音按钮和音量条)
|
// 轨道列表适配器
|
||||||
private class TrackAdapter extends android.widget.BaseAdapter {
|
private class TrackAdapter extends android.widget.BaseAdapter {
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
private MidiPlayer mPlayer;
|
private MidiPlayer mPlayer;
|
||||||
@@ -221,14 +384,13 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View getView(final int position, View convertView, ViewGroup parent) {
|
public View getView(final int position, View convertView, ViewGroup parent) {
|
||||||
// 加载轨道项布局(需创建item_track.xml)
|
|
||||||
View view = getLayoutInflater().inflate(R.layout.item_track, parent, false);
|
View view = getLayoutInflater().inflate(R.layout.item_track, parent, false);
|
||||||
TextView trackNameTv = (TextView) view.findViewById(R.id.tv_track_name);
|
TextView trackNameTv = (TextView) view.findViewById(R.id.tv_track_name);
|
||||||
final Button muteBtn = (Button) view.findViewById(R.id.btn_mute);
|
final Button muteBtn = (Button) view.findViewById(R.id.btn_mute);
|
||||||
SeekBar volumeSb = (SeekBar) view.findViewById(R.id.sb_volume);
|
SeekBar volumeSb = (SeekBar) view.findViewById(R.id.sb_volume);
|
||||||
|
|
||||||
// 设置轨道名称
|
|
||||||
trackNameTv.setText("轨道 " + (position + 1));
|
trackNameTv.setText("轨道 " + (position + 1));
|
||||||
|
muteBtn.setText("静音");
|
||||||
|
|
||||||
// 静音按钮逻辑
|
// 静音按钮逻辑
|
||||||
muteBtn.setOnClickListener(new View.OnClickListener() {
|
muteBtn.setOnClickListener(new View.OnClickListener() {
|
||||||
@@ -241,11 +403,11 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 音量条(示例:实际需结合MIDI事件处理)
|
// 音量条(预留逻辑)
|
||||||
volumeSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
volumeSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
// 可添加音量控制逻辑(如修改MIDI音量事件)
|
// 可添加音量控制逻辑(如发送MIDI音量事件)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -267,7 +429,6 @@ public class MidiPlayerActivity extends WinBoLLActivity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
// 释放资源
|
|
||||||
if (mMidiPlayer != null) {
|
if (mMidiPlayer != null) {
|
||||||
mMidiPlayer.stop();
|
mMidiPlayer.stop();
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package cc.winboll.studio.miniplayer;
|
package cc.winboll.studio.midiplayer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@188.com>
|
* @Author ZhanGSKen<zhangsken@188.com>
|
||||||
@@ -64,6 +64,21 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="停止"/>
|
android:text="停止"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_test"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="测试音符"
|
||||||
|
android:layout_below="@id/btn_stop"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |