
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); // 简写 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 instrument 0; int tempo 120; int keyOffset 0; int ticksPerQuarter 480; // 每四分音符的tick数 } static class NoteEvent { int note; int ticks; // 持续时间tick数 } private static JianPuConfig parseConfig(String configStr) { JianPuConfig config new JianPuConfig(); String cleaned configStr.trim(); if (cleaned.startsWith([)) { cleaned cleaned.substring(1); } if (cleaned.endsWith(])) { cleaned cleaned.substring(0, cleaned.length() - 1); } String[] parts cleaned.split(,); if (parts.length 3) { try { config.instrument Integer.parseInt(parts[0].trim()); if (config.instrument 0 || config.instrument 127) { config.instrument 0; } } catch (NumberFormatException e) { config.instrument 0; } try { config.tempo Integer.parseInt(parts[1].trim()); if (config.tempo 0) config.tempo 120; if (config.tempo 500) config.tempo 500; } catch (NumberFormatException e) { config.tempo 120; } String keyName parts[2].trim(); if (KEY_SIGNATURES.containsKey(keyName)) { config.keyOffset KEY_SIGNATURES.get(keyName); } } return config; } 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; } } 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 int getDurationMultiplier(String token) { int count 1; for (char c : token.toCharArray()) { if (c -) count; } return count; } // 解析简谱内容返回音符列表每个音符包含音高和时长 private static ListNoteEvent parseContent(String content, int keyOffset, int ticksPerQuarter) { 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拍 String groupStr group.toString(); // 计算组内有多少个有效音符 int noteCount 0; for (int j 0; j groupStr.length(); j) { char ch groupStr.charAt(j); if (ch 1 ch 7) noteCount; } if (noteCount 0) { int ticksPerNote ticksPerQuarter / noteCount; if (ticksPerNote 10) ticksPerNote 10; // 重新解析组内音符 int k 0; while (k groupStr.length()) { char ch groupStr.charAt(k); if (ch 1 ch 7) { StringBuilder token new StringBuilder(); token.append(ch); k; while (k groupStr.length()) { char next groupStr.charAt(k); if (next # || next \ || next . || next -) { token.append(next); k; } else { break; } } int midiNote parseNoteToken(token.toString(), keyOffset); if (midiNote ! -1 midiNote ! 0) { int mult getDurationMultiplier(token.toString()); NoteEvent ev new NoteEvent(); ev.note midiNote; ev.ticks ticksPerNote * mult; events.add(ev); } } else { k; } } } } 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拍 String groupStr group.toString(); int noteCount 0; for (int j 0; j groupStr.length(); j) { char ch groupStr.charAt(j); if (ch 1 ch 7) noteCount; } if (noteCount 0) { int ticksPerNote (ticksPerQuarter / 2) / noteCount; if (ticksPerNote 10) ticksPerNote 10; int k 0; while (k groupStr.length()) { char ch groupStr.charAt(k); if (ch 1 ch 7) { StringBuilder token new StringBuilder(); token.append(ch); k; while (k groupStr.length()) { char next groupStr.charAt(k); if (next # || next \ || next . || next -) { token.append(next); k; } else { break; } } int midiNote parseNoteToken(token.toString(), keyOffset); if (midiNote ! -1 midiNote ! 0) { int mult getDurationMultiplier(token.toString()); NoteEvent ev new NoteEvent(); ev.note midiNote; ev.ticks ticksPerNote * mult; events.add(ev); } } else { k; } } } } 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) { continue; } if (midiNote 0) { // 休止符添加静音事件 int mult getDurationMultiplier(tokenStr); NoteEvent ev new NoteEvent(); ev.note 0; ev.ticks ticksPerQuarter * mult; events.add(ev); } else { int mult getDurationMultiplier(tokenStr); NoteEvent ev new NoteEvent(); ev.note midiNote; ev.ticks ticksPerQuarter * mult; events.add(ev); } continue; } i; } return events; } public static MidiFile buildMidiFromJianPu(String jianpuText) { if (jianpuText null || jianpuText.trim().isEmpty()) { return null; } String text jianpuText.trim(); int configStart text.indexOf([); int configEnd text.indexOf(]); JianPuConfig config new JianPuConfig(); String content text; if (configStart 0 configEnd configStart) { String configStr text.substring(configStart, configEnd 1); config parseConfig(configStr); if (configEnd 1 text.length()) { content text.substring(configEnd 1); } else { content ; } } else { content text; } if (content.trim().isEmpty()) { return null; } ListNoteEvent noteEvents parseContent(content, config.keyOffset, config.ticksPerQuarter); if (noteEvents.isEmpty()) { return null; } // 构建MIDI MidiFile midi new MidiFile(); midi.format 1; midi.division config.ticksPerQuarter; 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); System.out.println(️ BPM: config.tempo , 每拍tick: config.ticksPerQuarter); // 生成音符事件 - 使用delta time long currentTick 0; int noteCount 0; int silentCount 0; for (NoteEvent ev : noteEvents) { if (ev.note 0) { // 休止符只增加时间 currentTick ev.ticks; silentCount; continue; } // Note On: delta time 是从上一个事件到当前事件的时间 MidiEvent noteOn new MidiEvent(currentTick); noteOn.status 0x90; noteOn.channel 0; noteOn.msgType 0x9; noteOn.data new int[] {ev.note, 80}; track.events.add(noteOn); // 重置currentTick为0表示这个音符的持续时间 currentTick 0; // Note Off: delta time 是音符持续时间 MidiEvent noteOff new MidiEvent(ev.ticks); noteOff.status 0x90; noteOff.channel 0; noteOff.msgType 0x9; noteOff.data new int[] {ev.note, 0}; track.events.add(noteOff); noteCount; } // 添加一个结束事件防止播放器提前结束 MidiEvent endEv new MidiEvent(0); endEv.isMeta true; endEv.metaType 0x2F; endEv.metaData new byte[0]; track.events.add(endEv); midi.tracks.add(track); System.out.println( 音符: noteCount , 休止: silentCount); return midi; } // 默认小星星 public static MidiFile buildDefaultMelody() { String star [0,220,G大调]5-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播放器 (时值修复版) ); 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文件启用【简谱输入模式】); } 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( 音色: 0-127 (0大钢琴)); System.out.println( 速度: BPM (如: 120, 220, 300)); System.out.println( 调式: C大调, G大调, A大调...); System.out.println(); System.out.println(【示例】); System.out.println([0,220,G大调]5-351--76-1-5---); System.out.println(); System.out.println(【音符规则】); System.out.println( 1-7: 基本音符 (每个占1拍)); System.out.println( -: 延音 (每个-延长1拍)); System.out.println( : 高八度 .: 低八度); System.out.println( #: 升半音); System.out.println( [12]: 组内共占1拍); System.out.println( (5533): 组内共占1/2拍); System.out.println( 0: 休止符); System.out.println(); System.out.println(⌨️ 输入 q 退出程序\n); String tempMid getMusicDir() /_tmp_play.mid; int playCount 0; while (true) { System.out.print( 请输入简谱 (q退出): ); String line readLine(); if (line null || line.trim().equalsIgnoreCase(q)) { System.out.println( 再见); break; } String jianpuText line.trim(); if (jianpuText.isEmpty()) { System.out.println(⚠️ 输入为空使用默认小星星); jianpuText [0,220,C大调]5-351--76-1-5---5-123-212----; } MidiFile midi buildMidiFromJianPu(jianpuText); if (midi null || midi.tracks.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); } } } }