修复MidiPlayer项目文件夹命名错误问题。

This commit is contained in:
ZhanGSKen
2025-06-29 12:04:31 +08:00
186 changed files with 1049 additions and 400 deletions

View File

@@ -22,7 +22,7 @@ android {
buildToolsVersion "32.0.0"
defaultConfig {
applicationId "cc.winboll.studio.miniplayer"
applicationId "cc.winboll.studio.midiplayer"
minSdkVersion 26
targetSdkVersion 30
versionCode 1

View File

@@ -1,8 +1,8 @@
#Created by .winboll/winboll_app_build.gradle
#Sun Jun 29 04:03:38 GMT 2025
stageCount=0
#Tue Sep 02 12:52:51 GMT 2025
stageCount=1
libraryProject=
baseVersion=15.0
publishVersion=15.0.0
buildCount=27
buildCount=2
baseBetaVersion=15.0.1

View File

@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest
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"/>
@@ -23,20 +23,6 @@
android:resizeableActivity="true"
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
android:name=".MidiPlayerActivity"
android:exported="true">

Binary file not shown.

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
import android.app.Activity;
import android.content.ClipData;

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
import android.app.Activity;
import android.os.Bundle;

View File

@@ -1,35 +1,35 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>
* @Date 2025/06/29 10:26
* @Describe MIDI文件解析器用于解析MIDI文件并提取轨道信息
*/
import android.util.Log;
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";
// 替代StandardCharsets.US_ASCII兼容低版本
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文件返回轨道数组
* 解析MIDI文件返回包含轨道和每拍ticks数的结果支持速度控制
*/
public MidiTrack[] parse() throws IOException {
public MidiPlayer.MidiParseResult parseWithTicks() throws IOException {
try {
// 1. 验证MIDI文件头MThd
if (!verifyHeader()) {
@@ -37,15 +37,39 @@ public class MidiParser {
return null;
}
// 2. 读取文件头信息包含轨道数量
// 2. 读取文件头信息包含每拍ticks数
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];
for (int i = 0; i < mTrackCount; i++) {
tracks[i] = parseTrack();
}
return tracks;
} finally {
if (mInputStream != null) {
@@ -64,7 +88,6 @@ public class MidiParser {
LogUtils.d(TAG, "文件头读取不完整,读取字节数: " + read);
return false;
}
// 使用US-ASCII编码验证兼容低版本
String headerStr = new String(header, US_ASCII);
boolean isValid = "MThd".equals(headerStr);
if (!isValid) {
@@ -75,14 +98,14 @@ public class MidiParser {
}
/**
* 读取MIDI文件头信息修复文件指针偏移问题
* 读取MIDI文件头信息提取每拍ticks数
*/
private void readHeaderInfo() throws IOException {
// 1. 读取头长度4字节标准MIDI固定为6
int headerLength = readInt();
LogUtils.d(TAG, "MIDI文件头长度: " + headerLength);
// 2. 读取头数据共6字节格式类型2字节 + 轨道数量2字节 + 时间分隔符2字节
// 2. 读取头数据共6字节
byte[] headerData = new byte[6];
int read = mInputStream.read(headerData);
if (read != 6) {
@@ -90,45 +113,128 @@ public class MidiParser {
throw new IOException("无效的MIDI文件头数据");
}
// 3. 从headerData中解析信息避免多次read导致指针偏移
// 3. 解析信息格式类型轨道数每拍ticks数
int formatType = ((headerData[0] & 0xFF) << 8) | (headerData[1] & 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, "时间分隔符(每拍 ticks): " + ticksPerBeat);
LogUtils.d(TAG, "时间分隔符(每拍 ticks): " + mTicksPerBeat);
LogUtils.d(TAG, "解析到轨道数量: " + mTrackCount);
// 4. 处理扩展头若头长度大于6跳过剩余字节
// 4. 处理扩展头
if (headerLength > 6) {
long skipped = mInputStream.skip(headerLength - 6);
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped + " (预期: " + (headerLength - 6) + ")");
LogUtils.d(TAG, "跳过扩展头字节数: " + skipped);
}
}
/**
* 解析单个轨道信息
* 解析单个轨道包含事件的deltaTicks用于速度控制
*/
private MidiTrack parseTrack() throws IOException {
MidiTrack track = new MidiTrack();
// 1. 读取轨道头4字节
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 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);
if (!"MTrk".equals(headerStr)) {
LogUtils.d(TAG, "无效的轨道头标识: " + headerStr
+ " (十六进制: " + bytesToHex(trackHeader) + ")");
return track;
return new MidiTrack();
}
// 3. 读取轨道长度4字节
// 3. 读取轨道长度
int trackLength = readInt();
LogUtils.d(TAG, "解析轨道,长度: " + trackLength + "字节");
@@ -145,6 +251,7 @@ public class MidiParser {
}
// 5. 解析轨道事件
MidiTrack track = new MidiTrack();
if (totalRead == trackLength) {
parseEvents(track, trackData);
} else {
@@ -155,7 +262,7 @@ public class MidiParser {
}
/**
* 解析MIDI事件按MIDI协议规范
* 原始事件解析方法兼容旧逻辑
*/
private void parseEvents(MidiTrack track, byte[] trackData) {
int offset = 0;
@@ -209,9 +316,9 @@ public class MidiParser {
return 1;
case 0xF: // 系统事件
if (statusByte == 0xFF) { // 元事件
return 1; // 简化处理实际需根据元事件类型判断
return 1;
} else if (statusByte == 0xF0 || statusByte == 0xF7) { // 系统专属事件
return 0; // 复杂处理暂不支持
return 0;
}
default:
return 0;
@@ -219,7 +326,7 @@ public class MidiParser {
}
/**
* 读取可变长度整数MIDI时间戳编码
* 读取可变长度整数MIDI事件时间差deltaTicks
*/
private long readVariableLength(byte[] data, int offset) {
long value = 0;
@@ -273,5 +380,41 @@ public class MidiParser {
}
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;
}
}*/
}

View File

@@ -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.0API 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;
}
}

View File

@@ -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
* @Describe Midi 播放窗口
*/
import android.app.Activity;
import android.content.Context;
import android.media.midi.MidiInputPort;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
@@ -17,6 +18,7 @@ import android.widget.Button;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
import cc.winboll.studio.libappbase.LogUtils;
import com.hjq.toast.ToastUtils;
import java.io.File;
import java.util.ArrayList;
@@ -27,12 +29,12 @@ public class MidiPlayerActivity extends WinBoLLActivity {
public static final String TAG = "MidiPlayerActivity";
private MidiPlayer mMidiPlayer;
private Button mPlayBtn, mPauseBtn, mStopBtn;
private Button mPlayBtn, mPauseBtn, mStopBtn, mTestBtn;
private ListView mTrackListView;
private ListView mFileListView; // 文件列表
private ListView mFileListView;
private TrackAdapter mTrackAdapter;
private TextView mFileNameTv;
private List<File> mMidiFileList = new ArrayList<>(); // 存储midi文件列表
private List<File> mMidiFileList = new ArrayList<>();
@Override
public Activity getActivity() {
@@ -47,31 +49,194 @@ public class MidiPlayerActivity extends WinBoLLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_midi_player); // 需确保布局文件存在
setContentView(R.layout.activity_midi_player);
// 初始化控件
mPlayBtn = (Button) findViewById(R.id.btn_play);
mPauseBtn = (Button) findViewById(R.id.btn_pause);
mStopBtn = (Button) findViewById(R.id.btn_stop);
mTestBtn = (Button) findViewById(R.id.btn_test);
mTrackListView = (ListView) findViewById(R.id.lv_tracks);
mFileListView = (ListView) findViewById(R.id.lv_midi_files);
mFileNameTv = (TextView) findViewById(R.id.tv_file_name);
// 检查系统版本兼容性
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
initMidiPlayer();
loadMidiFileList(); // 加载本地MIDI文件
loadMidiFileList();
initControlButtons();
initTestButton();
} else {
mFileNameTv.setText("当前设备不支持MIDI播放需Android 6.0+");
mFileNameTv.setText("当前设备不支持MIDI播放需Android 6.0及以上");
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() {
mMidiPlayer = new MidiPlayer(this);
// 设置合成器连接状态回调Java 7使用匿名内部类替代Lambda
mMidiPlayer.setOnSynthConnectedListener(new MidiPlayer.OnSynthConnectedListener() {
@Override
public void onConnected(final boolean success) {
@@ -92,14 +257,13 @@ public class MidiPlayerActivity extends WinBoLLActivity {
// 加载本地MIDI文件列表
private void loadMidiFileList() {
File midiDir = new File(getFilesDir(), "midi");
// 检查目录是否存在
if (!midiDir.exists()) {
midiDir.mkdirs(); // 创建目录
mFileNameTv.setText("midi目录已创建请放入文件");
midiDir.mkdirs();
mFileNameTv.setText("midi目录已创建等待文件拷贝...");
return;
}
// 筛选.mid和.midi文件
mMidiFileList.clear();
File[] files = midiDir.listFiles();
if (files != null) {
for (File file : files) {
@@ -110,39 +274,37 @@ public class MidiPlayerActivity extends WinBoLLActivity {
}
}
// 显示文件列表
if (mMidiFileList.isEmpty()) {
mFileNameTv.setText("midi目录中无可用文件");
} else {
showFileList();
mFileNameTv.setText("找到" + mMidiFileList.size() + "个MIDI文件");
}
}
// 显示文件列表到ListView
// 显示MIDI文件列表
private void showFileList() {
List<String> fileNameList = new ArrayList<>();
for (File file : mMidiFileList) {
fileNameList.add(file.getName());
}
// 设置文件列表适配器
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
fileNameList
this,
android.R.layout.simple_list_item_1,
fileNameList
);
mFileListView.setAdapter(adapter);
// 文件点击事件加载选中的MIDI文件Java 7使用匿名内部类替代Lambda
mFileListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mMidiPlayer.stop(); // 停止当前播放
mMidiPlayer.stop();
File selectedFile = mMidiFileList.get(position);
boolean loaded = mMidiPlayer.loadMidiFile(selectedFile);
if (loaded) {
mFileNameTv.setText("当前文件:" + selectedFile.getName());
initTrackList(); // 刷新轨道列表
initTrackList();
ToastUtils.show("已加载:" + selectedFile.getName());
} else {
mFileNameTv.setText("文件加载失败:" + selectedFile.getName());
@@ -151,7 +313,7 @@ public class MidiPlayerActivity extends WinBoLLActivity {
});
}
// 初始化轨道列表显示所有轨道的控制项
// 初始化轨道列表
private void initTrackList() {
mTrackAdapter = new TrackAdapter(this, mMidiPlayer);
mTrackListView.setAdapter(mTrackAdapter);
@@ -187,14 +349,15 @@ public class MidiPlayerActivity extends WinBoLLActivity {
});
}
// 禁用所有控制按钮
// 禁用所有按钮低版本设备
private void disableButtons() {
mPlayBtn.setEnabled(false);
mPauseBtn.setEnabled(false);
mStopBtn.setEnabled(false);
mTestBtn.setEnabled(false);
}
// 轨道控制适配器每个轨道显示静音按钮和音量条
// 轨道列表适配器
private class TrackAdapter extends android.widget.BaseAdapter {
private Context mContext;
private MidiPlayer mPlayer;
@@ -221,14 +384,13 @@ public class MidiPlayerActivity extends WinBoLLActivity {
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
// 加载轨道项布局需创建item_track.xml
View view = getLayoutInflater().inflate(R.layout.item_track, parent, false);
TextView trackNameTv = (TextView) view.findViewById(R.id.tv_track_name);
final Button muteBtn = (Button) view.findViewById(R.id.btn_mute);
SeekBar volumeSb = (SeekBar) view.findViewById(R.id.sb_volume);
// 设置轨道名称
trackNameTv.setText("轨道 " + (position + 1));
muteBtn.setText("静音");
// 静音按钮逻辑
muteBtn.setOnClickListener(new View.OnClickListener() {
@@ -241,11 +403,11 @@ public class MidiPlayerActivity extends WinBoLLActivity {
}
});
// 音量条示例实际需结合MIDI事件处理
// 音量条预留逻辑
volumeSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// 可添加音量控制逻辑修改MIDI音量事件
// 可添加音量控制逻辑发送MIDI音量事件
}
@Override
@@ -267,7 +429,6 @@ public class MidiPlayerActivity extends WinBoLLActivity {
@Override
protected void onDestroy() {
super.onDestroy();
// 释放资源
if (mMidiPlayer != null) {
mMidiPlayer.stop();
}

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>

View File

@@ -1,4 +1,4 @@
package cc.winboll.studio.miniplayer;
package cc.winboll.studio.midiplayer;
/**
* @Author ZhanGSKen<zhangsken@188.com>

View File

@@ -64,6 +64,21 @@
android:layout_height="wrap_content"
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
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Some files were not shown because too many files have changed in this diff Show More