修复MidiPlayer项目文件夹命名错误问题。
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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"> | ||||
							
								
								
									
										
											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.content.ClipData; | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cc.winboll.studio.miniplayer; | ||||
| package cc.winboll.studio.midiplayer; | ||||
| 
 | ||||
| /** | ||||
|  * @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.os.Bundle; | ||||
| @@ -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; | ||||
|         } | ||||
|     }*/ | ||||
| } | ||||
| 
 | ||||
| @@ -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 | ||||
|  * @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(); | ||||
|         } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cc.winboll.studio.miniplayer; | ||||
| package cc.winboll.studio.midiplayer; | ||||
| 
 | ||||
| /** | ||||
|  * @Author ZhanGSKen<zhangsken@188.com> | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cc.winboll.studio.miniplayer; | ||||
| package cc.winboll.studio.midiplayer; | ||||
| 
 | ||||
| /** | ||||
|  * @Author ZhanGSKen<zhangsken@188.com> | ||||
| @@ -1,4 +1,4 @@ | ||||
| package cc.winboll.studio.miniplayer; | ||||
| package cc.winboll.studio.midiplayer; | ||||
| 
 | ||||
| /** | ||||
|  * @Author ZhanGSKen<zhangsken@188.com> | ||||
| @@ -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" | ||||
| 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 | 
 ZhanGSKen
					ZhanGSKen