import android.media.MediaPlayer; import android.os.Environment; import java.io.*; import java.util.*; public class Main { // GM音色表 private static final MapInteger, String GM_NAMES new HashMap(); static { GM_NAMES.put(0, 大钢琴); GM_NAMES.put(1, 明亮钢琴); GM_NAMES.put(2, 电钢琴); GM_NAMES.put(3, 电钢琴2); GM_NAMES.put(4, 钢片琴); GM_NAMES.put(5, 音乐盒); GM_NAMES.put(6, 颤音琴); GM_NAMES.put(7, 马林巴); GM_NAMES.put(8, 木琴); GM_NAMES.put(9, 管钟); for (int i 10; i 128; i) { GM_NAMES.put(i, GM音色 i); } } // 调式映射 private static final MapString, Integer KEY_SIGNATURES new HashMap(); static { KEY_SIGNATURES.put(C大调, 0); KEY_SIGNATURES.put(C#大调, 1); KEY_SIGNATURES.put(Db大调, 1); KEY_SIGNATURES.put(D大调, 2); KEY_SIGNATURES.put(D#大调, 3); KEY_SIGNATURES.put(Eb大调, 3); KEY_SIGNATURES.put(E大调, 4); KEY_SIGNATURES.put(F大调, 5); KEY_SIGNATURES.put(F#大调, 6); KEY_SIGNATURES.put(Gb大调, 6); KEY_SIGNATURES.put(G大调, 7); KEY_SIGNATURES.put(G#大调, 8); KEY_SIGNATURES.put(Ab大调, 8); KEY_SIGNATURES.put(A大调, 9); KEY_SIGNATURES.put(A#大调, 10); KEY_SIGNATURES.put(Bb大调, 10); KEY_SIGNATURES.put(B大调, 11); } public static String getGMName(int num) { return GM_NAMES.getOrDefault(num, GM音色 num); } // MIDI基础结构 static class MidiEvent { long deltaTime; int status; int channel; int msgType; int[] data; boolean isMeta; int metaType; byte[] metaData; MidiEvent(long delta) { this.deltaTime delta; } } static class MidiTrack { ListMidiEvent events new ArrayList(); } static class MidiFile { int format; int division; ListMidiTrack tracks new ArrayList(); } // 变长数值读写 static long readVarLen(DataInputStream in) throws IOException { long value 0; int byteVal; do { byteVal in.readUnsignedByte(); value (value 7) | (byteVal 0x7F); } while ((byteVal 0x80) ! 0); return value; } static void writeVarLen(DataOutputStream out, long value) throws IOException { if (value 0) { out.write(0); return; } ListInteger bytes new ArrayList(); while (value 0) { bytes.add(0, (int) (value 0x7F)); value 7; } for (int i 0; i bytes.size() - 1; i) { bytes.set(i, bytes.get(i) | 0x80); } for (int b : bytes) { out.write(b); } } // MIDI读写函数 public static MidiFile parseMidi(String filepath) throws IOException { try (DataInputStream in new DataInputStream(new BufferedInputStream(new FileInputStream(filepath)))) { MidiFile midi new MidiFile(); byte[] header new byte[4]; in.readFully(header); if (!new String(header).equals(MThd)) throw new IOException(非法MIDI); int headerLen in.readInt(); midi.format in.readUnsignedShort(); int numTracks in.readUnsignedShort(); midi.division in.readUnsignedShort(); if (headerLen 6) in.skipBytes(headerLen - 6); for (int t 0; t numTracks; t) { in.readFully(header); if (!new String(header).equals(MTrk)) throw new IOException(轨道异常); int trackLen in.readInt(); MidiTrack track new MidiTrack(); byte[] trackData new byte[trackLen]; in.readFully(trackData); ByteArrayInputStream bais new ByteArrayInputStream(trackData); DataInputStream trackIn new DataInputStream(bais); int runningStatus -1; while (bais.available() 0) { long delta readVarLen(trackIn); int eventType trackIn.readUnsignedByte(); MidiEvent event new MidiEvent(delta); if (eventType 0xFF) { event.isMeta true; event.metaType trackIn.readUnsignedByte(); long metaLen readVarLen(trackIn); event.metaData new byte[(int) metaLen]; trackIn.readFully(event.metaData); runningStatus -1; } else if (eventType 0xF0 || eventType 0xF7) { event.status eventType; long sysexLen readVarLen(trackIn); event.data new int[(int) sysexLen]; for (int i 0; i sysexLen; i) event.data[i] trackIn.readUnsignedByte(); runningStatus -1; } else { int status; if ((eventType 0x80) ! 0) { status eventType; runningStatus status; } else { if (runningStatus -1) throw new IOException(状态码错误); status runningStatus; } event.status status; event.channel status 0x0F; event.msgType (status 4) 0x0F; if ((eventType 0x80) 0) { if (event.msgType 0xC || event.msgType 0xD) { event.data new int[] {eventType}; } else { int p2 trackIn.readUnsignedByte(); event.data new int[] {eventType, p2}; } } else { int p1 trackIn.readUnsignedByte(); if (event.msgType 0xC || event.msgType 0xD) { event.data new int[] {p1}; } else { int p2 trackIn.readUnsignedByte(); event.data new int[] {p1, p2}; } } } track.events.add(event); } midi.tracks.add(track); } return midi; } } public static void writeMidi(MidiFile midi, String filepath) throws IOException { try (DataOutputStream out new DataOutputStream(new BufferedOutputStream(new FileOutputStream(filepath)))) { out.writeBytes(MThd); out.writeInt(6); out.writeShort(midi.format); out.writeShort(midi.tracks.size()); out.writeShort(midi.division); for (MidiTrack track : midi.tracks) { out.writeBytes(MTrk); ByteArrayOutputStream baos new ByteArrayOutputStream(); DataOutputStream trackOut new DataOutputStream(baos); for (MidiEvent event : track.events) { writeVarLen(trackOut, event.deltaTime); if (event.isMeta) { trackOut.write(0xFF); trackOut.write(event.metaType); writeVarLen(trackOut, event.metaData.length); trackOut.write(event.metaData); } else if (event.status 0xF0 || event.status 0xF7) { trackOut.write(event.status); writeVarLen(trackOut, event.data.length); for (int d : event.data) trackOut.write(d); } else { trackOut.write(event.status); for (int d : event.data) trackOut.write(d); } } byte[] trackData baos.toByteArray(); out.writeInt(trackData.length); out.write(trackData); } } } public static void changeAllInstruments(MidiFile midi, int instrument) { for (MidiTrack track : midi.tracks) { ListMidiEvent newEvents new ArrayList(); SetInteger channels new HashSet(); for (MidiEvent e : track.events) { if (!e.isMeta e.msgType 0xC) continue; newEvents.add(e); if (!e.isMeta) channels.add(e.channel); } ListMidiEvent head new ArrayList(); for (int ch : channels) { MidiEvent pc new MidiEvent(0); pc.status 0xC0 | ch; pc.channel ch; pc.msgType 0xC; pc.data new int[] {instrument}; head.add(pc); } track.events.clear(); track.events.addAll(head); track.events.addAll(newEvents); } } // // 【简谱解析模块 - 完整版】 // 支持配置行、升降号、多八度、分组、休止符等 // static class JianPuConfig { int keyOffset 0; // 调式偏移半音数 int instrument 0; // 乐器编号 int tempo 120; // 速度BPM int tickPerBeat 480; // 每拍tick数 } /** 解析配置行调式,乐器编号,速度 */ private static JianPuConfig parseConfigLine(String line) { JianPuConfig config new JianPuConfig(); String[] parts line.split(,); if (parts.length 3) { // 解析调式 String keyName parts[0].trim(); if (KEY_SIGNATURES.containsKey(keyName)) { config.keyOffset KEY_SIGNATURES.get(keyName); } else { System.err.println(⚠️ 未知调式: keyName 使用C大调); } // 解析乐器 try { config.instrument Integer.parseInt(parts[1].trim()); if (config.instrument 0 || config.instrument 127) { System.err.println(⚠️ 乐器编号超出范围使用0); config.instrument 0; } } catch (NumberFormatException e) { System.err.println(⚠️ 乐器编号格式错误使用0); config.instrument 0; } // 解析速度 try { config.tempo Integer.parseInt(parts[2].trim()); if (config.tempo 0) { System.err.println(⚠️ 速度必须大于0使用120); config.tempo 120; } } catch (NumberFormatException e) { System.err.println(⚠️ 速度格式错误使用120); config.tempo 120; } } else { System.err.println(⚠️ 配置行格式错误使用默认配置); } return config; } /** 解析单个音符token返回MIDI音高 支持#升号多八度 */ private static int parseNoteToken(String token, int keyOffset) { if (token null || token.isEmpty()) return -1; // 处理休止符 if (token.equals(0)) return 0; // 提取数字和修饰符 int shift 0; boolean isSharp false; String numStr ; int idx 0; // 提取数字 while (idx token.length()) { char c token.charAt(idx); if (c 1 c 7) { numStr c; idx; break; // 只取第一个数字 } else { break; } } if (numStr.isEmpty()) return -1; // 处理后续修饰符 while (idx token.length()) { char c token.charAt(idx); if (c #) { isSharp true; idx; } else if (c \) { shift 12; idx; } else if (c .) { shift - 12; idx; } else { break; } } // 计算基础音高中音160 int baseNote; switch (numStr) { case 1: baseNote 60; break; case 2: baseNote 62; break; case 3: baseNote 64; break; case 4: baseNote 65; break; case 5: baseNote 67; break; case 6: baseNote 69; break; case 7: baseNote 71; break; default: return -1; } // 应用调式偏移和八度偏移 int note baseNote keyOffset shift; // 应用升号 if (isSharp) { note 1; } return note; } /** 解析简谱内容生成音符事件列表 */ private static ListNoteEvent parseContent(String content, int keyOffset, int tickPerBeat) { ListNoteEvent events new ArrayList(); // 移除所有空格和分隔符| String cleaned content.replace( , ).replace(|, ); int i 0; while (i cleaned.length()) { char c cleaned.charAt(i); // 处理分组 [] if (c [) { i; StringBuilder group new StringBuilder(); while (i cleaned.length() cleaned.charAt(i) ! ]) { group.append(cleaned.charAt(i)); i; } if (i cleaned.length() cleaned.charAt(i) ]) { i; // 组内音符共占1拍 ListNoteEvent groupEvents parseNoteGroup(group.toString(), keyOffset, tickPerBeat, 1); events.addAll(groupEvents); } continue; } // 处理分组 () if (c () { i; StringBuilder group new StringBuilder(); while (i cleaned.length() cleaned.charAt(i) ! )) { group.append(cleaned.charAt(i)); i; } if (i cleaned.length() cleaned.charAt(i) )) { i; // 组内音符拍长减半每个音符占1/2拍 ListNoteEvent groupEvents parseNoteGroup(group.toString(), keyOffset, tickPerBeat, 0.5); events.addAll(groupEvents); } continue; } // 处理普通音符或休止符 if ((c 1 c 7) || c 0) { StringBuilder token new StringBuilder(); token.append(c); i; // 收集修饰符 while (i cleaned.length()) { char next cleaned.charAt(i); if (next # || next \ || next . || next -) { token.append(next); i; } else { break; } } String tokenStr token.toString(); int midiNote parseNoteToken(tokenStr, keyOffset); if (midiNote -1) { System.err.println(⚠️ 跳过无效音符: tokenStr); continue; } // 计算时长延音标记 int durationMultiplier 1; for (char ch : tokenStr.toCharArray()) { if (ch -) durationMultiplier; } // 0是休止符仍然添加但音高为0表示无声 NoteEvent event new NoteEvent(); event.note midiNote; event.duration tickPerBeat * durationMultiplier; events.add(event); continue; } i; // 跳过其他字符 } return events; } /** 解析音符组[]或()内部 */ private static ListNoteEvent parseNoteGroup( String group, int keyOffset, int tickPerBeat, double timeFactor) { ListNoteEvent events new ArrayList(); // 移除空格和分隔符 String cleaned group.replace( , ).replace(|, ); int totalDuration (int) (tickPerBeat * timeFactor); int notesCount 0; // 先统计有多少个有效音符 ListString tokens new ArrayList(); int i 0; while (i cleaned.length()) { char c cleaned.charAt(i); if ((c 1 c 7) || c 0) { StringBuilder token new StringBuilder(); token.append(c); i; while (i cleaned.length()) { char next cleaned.charAt(i); if (next # || next \ || next . || next -) { token.append(next); i; } else { break; } } tokens.add(token.toString()); } else { i; } } if (tokens.isEmpty()) return events; // 每个音符平均分配时长 int durationPerNote totalDuration / tokens.size(); if (durationPerNote 10) durationPerNote 10; // 最小时长 for (String token : tokens) { int midiNote parseNoteToken(token, keyOffset); if (midiNote -1) continue; // 计算延音 int durationMultiplier 1; for (char ch : token.toCharArray()) { if (ch -) durationMultiplier; } NoteEvent event new NoteEvent(); event.note midiNote; event.duration durationPerNote * durationMultiplier; events.add(event); } return events; } static class NoteEvent { int note; // MIDI音高0表示休止符 int duration; // 时长tick数 } /** 由简谱文本生成MIDI完整版 */ public static MidiFile buildMidiFromJianPu(String jianpuText) { if (jianpuText null || jianpuText.trim().isEmpty()) { return null; } String[] lines jianpuText.split(\n); if (lines.length 1) return null; // 解析配置行 JianPuConfig config parseConfigLine(lines[0].trim()); // 解析内容 StringBuilder content new StringBuilder(); for (int i 1; i lines.length; i) { content.append(lines[i]); } ListNoteEvent noteEvents parseContent(content.toString(), config.keyOffset, config.tickPerBeat); if (noteEvents.isEmpty()) { System.err.println(⚠️ 没有有效音符); return null; } // 构建MIDI文件 MidiFile midi new MidiFile(); midi.format 1; midi.division config.tickPerBeat; MidiTrack track new MidiTrack(); // 设置音色 MidiEvent programEv new MidiEvent(0); programEv.status 0xC0; programEv.channel 0; programEv.msgType 0xC; programEv.data new int[] {config.instrument}; track.events.add(programEv); // 设置速度微秒/四分音符 int tempo 60000000 / config.tempo; MidiEvent tempoEv new MidiEvent(0); tempoEv.isMeta true; tempoEv.metaType 0x51; tempoEv.metaData new byte[] { (byte) ((tempo 16) 0xFF), (byte) ((tempo 8) 0xFF), (byte) (tempo 0xFF) }; track.events.add(tempoEv); // 添加音符 int currentTime 0; for (NoteEvent ev : noteEvents) { if (ev.note 0) { // 休止符只增加时间 currentTime ev.duration; continue; } // Note On MidiEvent noteOn new MidiEvent(currentTime - 0); noteOn.status 0x90; noteOn.channel 0; noteOn.msgType 0x9; noteOn.data new int[] {ev.note, 80}; track.events.add(noteOn); // Note Off currentTime ev.duration; MidiEvent noteOff new MidiEvent(0); noteOff.status 0x90; noteOff.channel 0; noteOff.msgType 0x9; noteOff.data new int[] {ev.note, 0}; track.events.add(noteOff); } midi.tracks.add(track); return midi; } // 默认小星星带配置行 public static MidiFile buildDefaultMelody() { String star G 大调,0,200\n5-351--76-1-5---5-123-212----; return buildMidiFromJianPu(star); } // 控制台输入 private static String readLine() { try { BufferedReader r new BufferedReader(new InputStreamReader(System.in)); return r.readLine(); } catch (Exception e) { return ; } } // 文件、播放工具 private static String getMusicDir() { return Environment.getExternalStorageDirectory() /Music; } private static ListFile scanMidi() { ListFile list new ArrayList(); File dir new File(getMusicDir()); if (!dir.exists()) return list; File[] fs dir.listFiles((d, name) - name.endsWith(.mid) || name.endsWith(.midi)); if (fs ! null) list.addAll(Arrays.asList(fs)); return list; } private static void playFile(String path) { try { MediaPlayer mp new MediaPlayer(); mp.setDataSource(path); mp.prepare(); mp.start(); System.out.println( 播放开始...); while (mp.isPlaying()) { Thread.sleep(100); } mp.release(); System.out.println(⏹️ 播放结束); } catch (Exception e) { System.out.println(⚠️ 播放异常: e.getMessage()); } } // 主入口 public static void main(String[] args) { System.out.println( 简谱MIDI播放器 (专业版) ); // 扫描外部MIDI文件 ListFile midiList scanMidi(); MidiFile sourceMidi null; if (!midiList.isEmpty()) { try { sourceMidi parseMidi(midiList.get(0).getAbsolutePath()); System.out.println( 载入外部MIDI文件 midiList.get(0).getName()); } catch (Exception e) { sourceMidi null; System.out.println(⚠️ 外部MIDI读取失败切换简谱模式); } } else { System.out.println( 未检测到外部mid文件启用【简谱输入模式】); } // 如果是外部MIDI直接播放并退出 if (sourceMidi ! null) { System.out.println(\n️ 请输入音色编号(0~127)); BufferedReader r new BufferedReader(new InputStreamReader(System.in)); try { int tone Integer.parseInt(r.readLine().trim()); if (tone 0 || tone 127) { System.out.println(❌ 音色错误退出); return; } System.out.println(✅ 选择音色 tone getGMName(tone)); changeAllInstruments(sourceMidi, tone); String tempMid getMusicDir() /_tmp_play.mid; writeMidi(sourceMidi, tempMid); System.out.println( 播放外部MIDI文件...); playFile(tempMid); new File(tempMid).delete(); System.out.println( 临时文件清理完成); } catch (Exception e) { System.out.println(❌ 错误: e.getMessage()); } return; } // 简谱循环播放模式 System.out.println(\n 简谱输入说明 ); System.out.println(【配置行】调式,乐器编号,速度); System.out.println( 调式: C大调, C#大调, D大调, ... G大调, A大调, B大调); System.out.println( 乐器: 0大钢琴, 1明亮钢琴, 2电钢琴 ... 127); System.out.println( 速度: BPM数值(越大越快)); System.out.println(); System.out.println(【音符】1-7, 加#升半音, 高八度, .低八度); System.out.println( 示例: 1 高音, 1.. 低两个八度, 5# 升5); System.out.println( 休止符: 0); System.out.println( 延音: - 延长一拍, --- 延长三拍); System.out.println( 分组: [12] 组内共占1拍, (5533) 组内拍长减半); System.out.println( 分隔: | 仅用于阅读); System.out.println(); System.out.println(【示例】); System.out.println(G 大调,0,200); System.out.println(5-351--76-1-5---5-123-212----); System.out.println(); System.out.println( 支持粘贴多行简谱包含配置行); System.out.println( 输入空行使用默认小星星); System.out.println(⌨️ 输入 q 退出程序\n); String tempMid getMusicDir() /_tmp_play.mid; int playCount 0; while (true) { System.out.print( 请输入简谱 (多行空行结束 / q退出): ); // 读取多行输入 StringBuilder input new StringBuilder(); String line; int emptyLineCount 0; try { BufferedReader r new BufferedReader(new InputStreamReader(System.in)); while ((line r.readLine()) ! null) { if (line.trim().equalsIgnoreCase(q)) { System.out.println( 再见); return; } if (line.trim().isEmpty()) { emptyLineCount; if (emptyLineCount 1) { break; } continue; } emptyLineCount 0; input.append(line).append(\n); } } catch (Exception e) { System.out.println(⚠️ 输入错误: e.getMessage()); continue; } String jianpuText input.toString().trim(); // 空输入使用默认 if (jianpuText.isEmpty()) { System.out.println(⚠️ 输入为空使用默认小星星); jianpuText G 大调,0,200\n5-351--76-1-5---5-123-212----; } // 解析并生成MIDI MidiFile midi buildMidiFromJianPu(jianpuText); if (midi null || midi.tracks.isEmpty() || midi.tracks.get(0).events.isEmpty()) { System.out.println(⚠️ 解析失败请检查格式); continue; } playCount; System.out.println(✅ 第 playCount 轮 - 简谱解析成功); try { writeMidi(midi, tempMid); System.out.println( 开始播放...); playFile(tempMid); new File(tempMid).delete(); System.out.println( 临时文件清理完成); System.out.println(--- 播放结束 ---\n); } catch (Exception e) { System.out.println(❌ 生成MIDI失败: e.getMessage()); System.out.println( 请重新输入简谱\n); } } } }