手机端安卓版的16音轨MIDI简谱播放器软件代码
```java
package com.example.miditest;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.SimpleAdapter;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.billthefarmer.mididriver.MidiDriver;
public class MainActivity extends AppCompatActivity implements MidiDriver.OnMidiStartListener {
private MidiDriver midiDriver;
private byte[] event;
private int[] config;
private Button buttonExport;
private FrameLayout trackContainer;
private GridView trackButtonsGrid;
private Button buttonPlay;
private Button buttonStop;
private CheckBox checkBoxLoop;
private Handler handler;
private boolean isPlaying = false;
private boolean shouldLoop = false;
private List<List<NoteEvent>> allTrackEvents = new ArrayList<>();
private int[] currentNoteIndices = new int[16];
private EditText[] trackEditTexts = new EditText[16];
private List<Integer> trackTempos = new ArrayList<>();
private List<String> trackKeys = new ArrayList<>();
private List<Runnable> trackRunnables = new ArrayList<>();
// 默认参数
private int defaultTempo = 120;
private int defaultInstrument = 0;
private String defaultKey = "C大调";
// 调式映射表
private static final HashMap<String, Integer> KEY_SIGNATURES = new HashMap<String, Integer>() {{
// 大调
put("C大调", 0); put("C", 0);
put("G大调", 7); put("G", 7);
put("D大调", 2); put("D", 2);
put("A大调", -3); put("A", -3);
put("E大调", 4); put("E", 4);
put("B大调", -1); put("B", -1);
put("F#大调", 6); put("F#", 6); put("F♯大调", 6); put("F♯", 6);
put("C#大调", 1); put("C#", 1); put("C♯大调", 1); put("C♯", 1);
put("F大调", -5); put("F", -5);
put("降B大调", -2); put("Bb大调", -2); put("降B", -2); put("Bb", -2); put("B♭大调", -2); put("B♭", -2);
put("降E大调", -4); put("Eb大调", -4); put("降E", -4); put("Eb", -4); put("E♭大调", -4); put("E♭", -4);
put("降A大调", 3); put("Ab大调", 3); put("降A", 3); put("Ab", 3); put("A♭大调", 3); put("A♭", 3);
put("降D大调", 5); put("Db大调", 5); put("降D", 5); put("Db", 5); put("D♭大调", 5); put("D♭", 5);
put("降G大调", -6); put("Gb大调", -6); put("降G", -6); put("Gb", -6); put("G♭大调", -6); put("G♭", -6);
// 小调(相对小调)
put("A小调", 0); put("Am", 0); put("a小调", 0); put("am", 0);
put("E小调", 7); put("Em", 7); put("e小调", 7); put("em", 7);
put("B小调", 2); put("Bm", 2); put("b小调", 2); put("bm", 2);
put("F#小调", -3); put("F#m", -3); put("f#小调", -3); put("f#m", -3); put("F♯小调", -3); put("F♯m", -3);
put("C#小调", 4); put("C#m", 4); put("c#小调", 4); put("c#m", 4); put("C♯小调", 4); put("C♯m", 4);
put("G#小调", -1); put("G#m", -1); put("g#小调", -1); put("g#m", -1); put("G♯小调", -1); put("G♯m", -1);
put("D#小调", 6); put("D#m", 6); put("d#小调", 6); put("d#m", 6); put("D♯小调", 6); put("D♯m", 6);
put("A#小调", 1); put("A#m", 1); put("a#小调", 1); put("a#m", 1); put("A♯小调", 1); put("A♯m", 1);
put("D小调", -5); put("Dm", -5); put("d小调", -5); put("dm", -5);
put("G小调", -2); put("Gm", -2); put("g小调", -2); put("gm", -2);
put("C小调", -4); put("Cm", -4); put("c小调", -4); put("cm", -4);
put("F小调", 3); put("Fm", 3); put("f小调", 3); put("fm", 3);
put("降B小调", 5); put("Bbm", 5); put("降Bm", 5); put("bb小调", 5); put("bbm", 5); put("B♭小调", 5); put("B♭m", 5);
put("降E小调", -6); put("Ebm", -6); put("降Em", -6); put("eb小调", -6); put("ebm", -6); put("E♭小调", -6); put("E♭m", -6);
}};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化视图
trackContainer = findViewById(R.id.trackContainer);
trackButtonsGrid = findViewById(R.id.trackButtonsGrid);
buttonPlay = findViewById(R.id.buttonPlay);
buttonStop = findViewById(R.id.buttonStop);
buttonExport = findViewById(R.id.buttonExport);
checkBoxLoop = findViewById(R.id.checkBoxLoop);
// 设置导出按钮点击监听
buttonExport.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
exportToMidi();
}
});
// 初始化16个音轨文本框
initTrackEditTexts();
// 初始化音轨选择按钮
initTrackButtons();
// 默认显示第一个音轨
trackEditTexts[0].setVisibility(View.VISIBLE);
for (int i = 1; i < 16; i++) {
trackEditTexts[i].setVisibility(View.GONE);
}
// 设置按钮点击监听
buttonPlay.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startPlaying();
}
});
buttonStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
stopPlaying();
}
});
checkBoxLoop.setOnCheckedChangeListener((buttonView, isChecked) -> {
shouldLoop = isChecked;
});
// 初始化Handler
handler = new Handler(Looper.getMainLooper());
// 实例化MIDI驱动
midiDriver = MidiDriver.getInstance();
midiDriver.setOnMidiStartListener(this);
}
private void initTrackEditTexts() {
// 第一个音轨文本框已经存在,获取引用
trackEditTexts[0] = findViewById(R.id.editTextTrack1);
// 创建其他15个音轨文本框,放在同一个位置
for (int i = 1; i < 16; i++) {
EditText editText = new EditText(this);
editText.setId(View.generateViewId());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
);
editText.setLayoutParams(params);
editText.setHint("音轨" + (i + 1) + ": [音色,速度,调式]简谱内容");
editText.setInputType(android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE);
editText.setMinLines(3);
editText.setVerticalScrollBarEnabled(true);
editText.setVisibility(View.GONE);
trackContainer.addView(editText);
trackEditTexts[i] = editText;
}
}
private void initTrackButtons() {
List<HashMap<String, String>> data = new ArrayList<>();
for (int i = 0; i < 16; i++) {
HashMap<String, String> map = new HashMap<>();
map.put("text", "音轨" + (i + 1));
data.add(map);
}
String[] from = {"text"};
int[] to = {android.R.id.text1};
SimpleAdapter adapter = new SimpleAdapter(this, data,
android.R.layout.simple_list_item_1, from, to);
trackButtonsGrid.setAdapter(adapter);
trackButtonsGrid.setOnItemClickListener((parent, view, position, id) -> {
toggleTrackVisibility(position);
});
}
private void toggleTrackVisibility(int trackIndex) {
if (trackIndex >= 0 && trackIndex < 16) {
for (int i = 0; i < 16; i++) {
trackEditTexts[i].setVisibility(View.GONE);
}
trackEditTexts[trackIndex].setVisibility(View.VISIBLE);
}
}
@Override
protected void onResume() {
super.onResume();
midiDriver.start();
config = midiDriver.config();
}
@Override
protected void onPause() {
super.onPause();
stopPlaying();
midiDriver.stop();
}
@Override
public void onMidiStart() {
Log.d("MidiTest", "MIDI started");
}
public void startPlaying() {
if (isPlaying) {
return;
}
// 解析所有音轨
allTrackEvents.clear();
trackTempos.clear();
trackKeys.clear();
trackRunnables.clear();
boolean hasValidTrack = false;
for (int i = 0; i < 16; i++) {
String musicText = trackEditTexts[i].getText().toString().trim();
if (TextUtils.isEmpty(musicText)) {
allTrackEvents.add(new ArrayList<>());
trackTempos.add(defaultTempo);
trackKeys.add(defaultKey);
continue;
}
ParsedMusic parsedMusic = parseMusicWithParameters(musicText);
if (!parsedMusic.noteEvents.isEmpty()) {
allTrackEvents.add(parsedMusic.noteEvents);
trackTempos.add(parsedMusic.tempo);
trackKeys.add(parsedMusic.key);
hasValidTrack = true;
selectInstrument(parsedMusic.instrument, i);
} else {
allTrackEvents.add(new ArrayList<>());
trackTempos.add(defaultTempo);
trackKeys.add(defaultKey);
}
}
if (!hasValidTrack) {
Toast.makeText(this, "没有有效的音轨内容", Toast.LENGTH_SHORT).show();
return;
}
for (int i = 0; i < 16; i++) {
currentNoteIndices[i] = 0;
}
isPlaying = true;
shouldLoop = checkBoxLoop.isChecked();
// 为每个音轨创建独立的播放任务
for (int i = 0; i < 16; i++) {
final int track = i;
if (!allTrackEvents.get(track).isEmpty()) {
Runnable trackRunnable = createTrackRunnable(track);
trackRunnables.add(trackRunnable);
handler.post(trackRunnable);
}
}
}
private Runnable createTrackRunnable(final int track) {
return new Runnable() {
@Override
public void run() {
if (!isPlaying) return;
List<NoteEvent> trackEvents = allTrackEvents.get(track);
if (trackEvents.isEmpty() || currentNoteIndices[track] >= trackEvents.size()) {
if (shouldLoop) {
currentNoteIndices[track] = 0;
} else {
return;
}
}
NoteEvent currentEvent = trackEvents.get(currentNoteIndices[track]);
String currentKey = trackKeys.get(track);
// 播放音符
if (currentEvent.notes != null && !currentEvent.notes.isEmpty()) {
for (String noteStr : currentEvent.notes) {
int noteNumber = convertToMidiNote(noteStr, currentKey);
if (noteNumber >= 0) {
playNote(noteNumber, track);
}
}
}
final int delay = currentEvent.duration;
// 安排停止和播放下一个音符
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!isPlaying) return;
// 停止当前音符
if (currentNoteIndices[track] < trackEvents.size()) {
NoteEvent event = trackEvents.get(currentNoteIndices[track]);
String eventKey = trackKeys.get(track);
if (event.notes != null) {
for (String noteStr : event.notes) {
int noteNumber = convertToMidiNote(noteStr, eventKey);
if (noteNumber >= 0) {
stopNote(noteNumber, track);
}
}
}
}
currentNoteIndices[track]++;
// 检查是否循环或继续播放
if (currentNoteIndices[track] >= trackEvents.size()) {
if (shouldLoop) {
currentNoteIndices[track] = 0;
} else {
// 检查所有音轨是否都结束
boolean allFinished = true;
for (int i = 0; i < 16; i++) {
if (!allTrackEvents.get(i).isEmpty() &&
currentNoteIndices[i] < allTrackEvents.get(i).size()) {
allFinished = false;
break;
}
}
if (allFinished) {
isPlaying = false;
return;
}
return;
}
}
// 继续播放下一个音符
handler.post(createTrackRunnable(track));
}
}, delay);
}
};
}
private void stopPlaying() {
isPlaying = false;
handler.removeCallbacksAndMessages(null);
trackRunnables.clear();
for (int track = 0; track < 16; track++) {
for (int i = 0; i < 128; i++) {
stopNote(i, track);
}
}
}
private void playAllTracksIndependently() {
if (!isPlaying) {
return;
}
// 检查是否所有音轨都已结束
if (checkAllTracksFinished()) {
if (shouldLoop) {
// 循环播放:重置所有音轨索引
for (int i = 0; i < 16; i++) {
currentNoteIndices[i] = 0;
}
playAllTracksIndependently();
} else {
isPlaying = false;
}
return;
}
// 为每个音轨安排独立的时间调度
for (int track = 0; track < 16; track++) {
final int currentTrack = track;
List<NoteEvent> trackEvents = allTrackEvents.get(currentTrack);
// 检查当前音轨是否有事件,以及当前索引是否有效
if (trackEvents.isEmpty() || currentNoteIndices[currentTrack] >= trackEvents.size()) {
continue; // 跳过已结束或无效的音轨
}
NoteEvent currentEvent = trackEvents.get(currentNoteIndices[currentTrack]);
// 播放当前音符
if (currentEvent.notes != null && !currentEvent.notes.isEmpty()) {
for (String noteStr : currentEvent.notes) {
// 在 playAllTracksIndependently() 方法中
int noteNumber = convertToMidiNote(noteStr, trackKeys.get(currentTrack));
if (noteNumber >= 0) {
playNote(noteNumber, currentTrack);
}
}
}
// 计算当前音符的持续时间
int delay = currentEvent.duration;
// 安排停止音符和播放下一个音符
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!isPlaying) return;
// 再次检查索引有效性
if (currentNoteIndices[currentTrack] >= trackEvents.size()) {
return;
}
// 停止当前音符
NoteEvent event = trackEvents.get(currentNoteIndices[currentTrack]);
if (event.notes != null) {
for (String noteStr : event.notes) {
int noteNumber = convertToMidiNote(noteStr, trackKeys.get(currentTrack));
if (noteNumber >= 0) {
stopNote(noteNumber, currentTrack);
}
}
}
// 前进到下一个音符
currentNoteIndices[currentTrack]++;
// 继续播放下一个音符
handler.post(() -> playAllTracksIndependently());
}
}, delay);
}
}
private boolean checkAllTracksFinished() {
for (int i = 0; i < 16; i++) {
List<NoteEvent> trackEvents = allTrackEvents.get(i);
if (!trackEvents.isEmpty() && currentNoteIndices[i] < trackEvents.size()) {
return false; // 至少还有一个音轨没播完
}
}
return true; // 所有音轨都结束了
}
private ParsedMusic parseMusicWithParameters(String musicText) {
ParsedMusic result = new ParsedMusic();
result.instrument = defaultInstrument;
result.tempo = defaultTempo;
result.key = defaultKey;
// 检查是否有参数部分 [instrument,tempo,key]
Pattern paramPattern = Pattern.compile("^\\[(\\d+),(\\d+),([^\\]]+)\\](.*)");
Matcher paramMatcher = paramPattern.matcher(musicText);
String actualMusicText;
if (paramMatcher.find()) {
// 提取参数
try {
result.instrument = Integer.parseInt(paramMatcher.group(1));
result.tempo = Integer.parseInt(paramMatcher.group(2));
result.key = paramMatcher.group(3);
actualMusicText = paramMatcher.group(4);
} catch (NumberFormatException e) {
// 参数格式错误,使用默认值
actualMusicText = musicText;
}
} else {
// 没有参数部分,使用整个文本作为简谱
actualMusicText = musicText;
}
// 解析简谱
result.noteEvents = parseMusicNotes(actualMusicText, result.tempo);
return result;
}
private List<NoteEvent> parseMusicNotes(String musicText, int tempo) {
List<NoteEvent> events = new ArrayList<>();
if (TextUtils.isEmpty(musicText)) {
return events;
}
// 移除空格和竖线分隔符
String cleanedText = musicText.replaceAll("\\s+", "").replaceAll("\\|", "");
// 计算每拍的毫秒数 (60000ms / 每分钟拍数)
int beatDuration = 60000 / tempo;
// 匹配模式:音符、休止符、和弦、减半时值
Pattern pattern = Pattern.compile("(\\d[#']*\\.*)|(0+)|(-+)|(\\[.*?\\])|(\\(.*?\\/)");
Matcher matcher = pattern.matcher(cleanedText);
while (matcher.find()) {
String group = matcher.group();
if (group.startsWith("[")) {
// 和弦处理:多个音符一起占一拍
String chordNotes = group.substring(1, group.length() - 1);
events.add(new NoteEvent(chordNotes, beatDuration, true));
} else if (group.startsWith("(")) {
// 减半时值处理:拍长减半
String fastNotes = group.substring(1, group.length() - 1);
events.add(new NoteEvent(fastNotes, beatDuration / 2, false));
} else if (group.startsWith("-")) {
// 休止符处理:延长拍
events.add(new NoteEvent(null, group.length() * beatDuration, false));
} else if (group.startsWith("0")) {
// 无声处理
events.add(new NoteEvent(null, group.length() * beatDuration, false));
} else {
// 单个音符处理
events.add(new NoteEvent(group, beatDuration, false));
}
}
return events;
}
private int convertToMidiNote(String noteStr, String key) {
if (noteStr == null || noteStr.isEmpty()) {
return -1;
}
// 确保只包含有效字符
if (!noteStr.matches("[0-7#'.]+")) {
return -1;
}
// 提取基础音符数字
char baseNoteChar = noteStr.charAt(0);
if (baseNoteChar < '0' || baseNoteChar > '7') {
return -1;
}
int baseNote;
switch (baseNoteChar) {
case '1': baseNote = 60; break; // C4
case '2': baseNote = 62; break; // D4
case '3': baseNote = 64; break; // E4
case '4': baseNote = 65; break; // F4
case '5': baseNote = 67; break; // G4
case '6': baseNote = 69; break; // A4
case '7': baseNote = 71; break; // B4
default: return -1;
}
// 处理升调 (#)
if (noteStr.contains("#")) {
baseNote++;
}
// 处理八度变化 (. 和 ')
int dotCount = countChars(noteStr, '.');
int quoteCount = countChars(noteStr, '\'');
int octaveChange = quoteCount - dotCount;
baseNote += octaveChange * 12;
// 应用调式偏移
Integer keyOffset = KEY_SIGNATURES.get(key);
if (keyOffset != null) {
baseNote += keyOffset;
}
// 确保音符在有效MIDI范围内 (0-127)
if (baseNote < 0) {
Log.w("MidiTest", "音符太低: " + noteStr + " -> MIDI " + baseNote + ", 已调整为21 (A0)");
return 21; // 调整为A0,这是大多数钢琴的最低音
} else if (baseNote > 127) {
Log.w("MidiTest", "音符太高: " + noteStr + " -> MIDI " + baseNote + ", 已调整为108 (C8)");
return 108; // 调整为C8,这是大多数钢琴的最高音
}
return baseNote;
}
// 添加一个方法来检查音符是否在可听范围内
private boolean isNoteAudible(int midiNote) {
// 只是记录警告,但不阻止播放
if (midiNote < 21) {
Log.w("MidiTest", "低音警告: MIDI " + midiNote + " 可能无法播放");
} else if (midiNote > 108) {
Log.w("MidiTest", "高音警告: MIDI " + midiNote + " 可能无法播放");
}
return true; // 总是尝试播放
}
// 如果需要更详细的调试信息,可以添加这个方法
private void debugNoteConversion(String noteStr, int midiNote) {
if (midiNote >= 0) {
String noteName = getNoteName(midiNote);
Log.d("MidiTest", "转换: " + noteStr + " -> MIDI " + midiNote + " (" + noteName + ")");
} else {
Log.d("MidiTest", "无效音符: " + noteStr);
}
}
// 可选:将MIDI音符编号转换为音符名称
private String getNoteName(int midiNote) {
String[] noteNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
int octave = (midiNote / 12) - 1;
int noteIndex = midiNote % 12;
return noteNames[noteIndex] + octave + " (" + midiNote + ")";
}
private int countChars(String str, char ch) {
int count = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == ch) {
count++;
}
}
return count;
}
private void playNote(int noteNumber, int track) {
if (!isNoteAudible(noteNumber)) {
Log.w("MidiTest", "跳过可能无法播放的音符: MIDI " + noteNumber + " (" + getNoteName(noteNumber) + ")");
return;
}
event = new byte[3];
event[0] = (byte) (0x90 | track); // Note On, 使用不同的通道
event[1] = (byte) noteNumber;
event[2] = (byte) 0x7F; // Maximum velocity
midiDriver.write(event);
Log.d("MidiTest", "播放音轨" + track + ": MIDI " + noteNumber + " (" + getNoteName(noteNumber) + ")");
}
private void stopNote(int noteNumber, int track) {
event = new byte[3];
event[0] = (byte) (0x80 | track); // Note Off, 使用不同的通道
event[1] = (byte) noteNumber;
event[2] = (byte) 0x00; // Minimum velocity
midiDriver.write(event);
}
private void selectInstrument(int instrument, int track) {
event = new byte[2];
event[0] = (byte)(0xC0 | track); // Program change, 使用不同的通道
event[1] = (byte)instrument;
midiDriver.write(event);
}
// 添加导出MIDI文件的方法
private void exportToMidi() {
try {
// 解析所有音轨
List<ParsedMusic> allParsedMusic = new ArrayList<>();
boolean hasValidTrack = false;
for (int i = 0; i < 16; i++) {
String musicText = trackEditTexts[i].getText().toString().trim();
if (TextUtils.isEmpty(musicText)) {
allParsedMusic.add(null);
continue;
}
ParsedMusic parsedMusic = parseMusicWithParameters(musicText);
if (!parsedMusic.noteEvents.isEmpty()) {
allParsedMusic.add(parsedMusic);
hasValidTrack = true;
} else {
allParsedMusic.add(null);
}
}
if (!hasValidTrack) {
Toast.makeText(this, "没有有效的音轨内容", Toast.LENGTH_SHORT).show();
return;
}
// 生成MIDI文件(使用格式1和时间缩放)
byte[] midiData = generateMidiData(allParsedMusic);
// 保存到文件
File file = new File(getExternalFilesDir(null), "composition.mid");
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(midiData);
Toast.makeText(this, "MIDI文件已保存: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show();
Log.d("MidiTest", "MIDI文件保存成功: " + file.getAbsolutePath());
}
} catch (Exception e) {
Log.e("MidiTest", "导出MIDI文件失败", e);
Toast.makeText(this, "导出失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
// 生成MIDI文件数据
private byte[] generateMidiData(List<ParsedMusic> allParsedMusic) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 计算有效音轨数量
int validTrackCount = 0;
for (ParsedMusic music : allParsedMusic) {
if (music != null && !music.noteEvents.isEmpty()) {
validTrackCount++;
}
}
if (validTrackCount == 0) {
throw new IOException("没有有效的音轨内容");
}
// 添加全局音轨
validTrackCount++;
// MIDI文件头 - 使用格式1(多音轨)
writeMidiHeader(baos, 1, validTrackCount, 480);
// 选择基准速度(使用第一个有效音轨的速度)
int baseTempo = 120;
for (ParsedMusic music : allParsedMusic) {
if (music != null && !music.noteEvents.isEmpty()) {
baseTempo = music.tempo;
break;
}
}
// 写入全局音轨(使用基准速度)
writeGlobalTrack(baos, baseTempo);
// 为每个有效音轨写入单独的音轨数据(使用时间缩放)
for (int track = 0; track < 16; track++) {
ParsedMusic music = allParsedMusic.get(track);
if (music != null && !music.noteEvents.isEmpty()) {
writeTrackWithTimeScaling(baos, music, track, baseTempo);
}
}
return baos.toByteArray();
}
private void writeGlobalTrack(ByteArrayOutputStream baos, int baseTempo) throws IOException {
ByteArrayOutputStream trackData = new ByteArrayOutputStream();
// 时间签名(4/4拍)
trackData.write(0x00); // delta time
trackData.write(0xFF); // Meta event
trackData.write(0x58); // Time signature
trackData.write(0x04); // Length
trackData.write(0x04); // 4/4拍
trackData.write(0x02);
trackData.write(0x18);
trackData.write(0x08);
// 设置基准速度
int microsecPerQuarterNote = 60000000 / baseTempo;
trackData.write(0x00); // delta time
trackData.write(0xFF); // Meta event
trackData.write(0x51); // Tempo
trackData.write(0x03); // Length
trackData.write((microsecPerQuarterNote >> 16) & 0xFF);
trackData.write((microsecPerQuarterNote >> 8) & 0xFF);
trackData.write(microsecPerQuarterNote & 0xFF);
// 音轨结束
trackData.write(0x00);
trackData.write(0xFF);
trackData.write(0x2F);
trackData.write(0x00);
// 写入全局音轨
baos.write("MTrk".getBytes());
byte[] trackBytes = trackData.toByteArray();
baos.write(intToBytes(trackBytes.length, 4));
baos.write(trackBytes);
}
// MIDI事件辅助类
private static class MidiEvent {
long deltaTime;
byte[] eventData;
}
private void writeTrack(ByteArrayOutputStream baos, ParsedMusic music, int track, int globalTempo) throws IOException {
ByteArrayOutputStream trackData = new ByteArrayOutputStream();
// 设置音色
trackData.write(0x00);
trackData.write((byte) (0xC0 | (track & 0x0F)));
trackData.write((byte) (music.instrument & 0x7F));
// 使用全局速度来计算时间
int ticksPerQuarterNote = 480;
int microsecondsPerQuarterNote = 60000000 / globalTempo;
int cumulativeDelta = 0;
for (NoteEvent event : music.noteEvents) {
// 使用全局速度计算ticks
int eventTicks = (int) ((event.duration * ticksPerQuarterNote * 1000.0) / microsecondsPerQuarterNote);
if (eventTicks <= 0) eventTicks = 1;
if (event.notes != null && !event.notes.isEmpty()) {
// Note On 事件
for (int i = 0; i < event.notes.size(); i++) {
String noteStr = event.notes.get(i);
int noteNumber = convertToMidiNote(noteStr, music.key);
if (noteNumber >= 0 && noteNumber <= 127) {
if (i == 0) {
writeVarLen(trackData, cumulativeDelta);
cumulativeDelta = 0;
} else {
writeVarLen(trackData, 0);
}
trackData.write((byte) (0x90 | (track & 0x0F)));
trackData.write((byte) noteNumber);
trackData.write((byte) 0x7F);
}
}
cumulativeDelta += eventTicks;
} else {
cumulativeDelta += eventTicks;
}
}
// 音轨结束
writeVarLen(trackData, cumulativeDelta);
trackData.write(0xFF);
trackData.write(0x2F);
trackData.write(0x00);
// 写入音轨
baos.write("MTrk".getBytes());
byte[] trackBytes = trackData.toByteArray();
baos.write(intToBytes(trackBytes.length, 4));
baos.write(trackBytes);
}
private void writeTrackWithTimeScaling(ByteArrayOutputStream baos, ParsedMusic music, int track, int baseTempo) throws IOException {
ByteArrayOutputStream trackData = new ByteArrayOutputStream();
// 设置音色
trackData.write(0x00); // delta time
trackData.write((byte) (0xC0 | (track & 0x0F))); // Program change
trackData.write((byte) (music.instrument & 0x7F));
// 计算速度比例因子
double tempoRatio = (double) baseTempo / music.tempo;
int ticksPerQuarterNote = 480;
int baseMicrosecPerQuarterNote = 60000000 / baseTempo;
long cumulativeDelta = 0;
List<Integer> activeNotes = new ArrayList<>();
for (NoteEvent event : music.noteEvents) {
// 计算原始ticks和缩放后的ticks
int originalTicks = (int) ((event.duration * ticksPerQuarterNote * 1000.0) / baseMicrosecPerQuarterNote);
int scaledTicks = (int) (originalTicks * tempoRatio);
if (scaledTicks <= 0) scaledTicks = 1;
if (event.notes != null && !event.notes.isEmpty()) {
// 先关闭所有当前活动的音符
if (!activeNotes.isEmpty()) {
writeVarLen(trackData, (int) cumulativeDelta);
cumulativeDelta = 0;
for (int note : activeNotes) {
trackData.write((byte) (0x80 | (track & 0x0F))); // Note Off
trackData.write((byte) note);
trackData.write((byte) 0x00); // velocity
}
activeNotes.clear();
}
// 打开新音符(使用正确的调式)
writeVarLen(trackData, (int) cumulativeDelta);
cumulativeDelta = 0;
for (String noteStr : event.notes) {
int noteNumber = convertToMidiNote(noteStr, music.key);
if (noteNumber >= 0 && noteNumber <= 127) {
trackData.write((byte) (0x90 | (track & 0x0F))); // Note On
trackData.write((byte) noteNumber);
trackData.write((byte) 0x7F); // velocity
activeNotes.add(noteNumber);
}
}
cumulativeDelta = scaledTicks;
} else {
// 休止符
cumulativeDelta += scaledTicks;
}
}
// 关闭所有剩余的音符
if (!activeNotes.isEmpty()) {
writeVarLen(trackData, (int) cumulativeDelta);
for (int note : activeNotes) {
trackData.write((byte) (0x80 | (track & 0x0F))); // Note Off
trackData.write((byte) note);
trackData.write((byte) 0x00); // velocity
}
}
// 音轨结束
trackData.write(0x00);
trackData.write(0xFF);
trackData.write(0x2F);
trackData.write(0x00);
// 写入音轨
baos.write("MTrk".getBytes());
byte[] trackBytes = trackData.toByteArray();
baos.write(intToBytes(trackBytes.length, 4));
baos.write(trackBytes);
}
// 写入MIDI文件头
private void writeMidiHeader(ByteArrayOutputStream baos, int format, int numTracks, int division) throws IOException {
// "MThd"
baos.write('M');
baos.write('T');
baos.write('h');
baos.write('d');
// 头部长度 (6 bytes)
baos.write(0x00);
baos.write(0x00);
baos.write(0x00);
baos.write(0x06);
// 格式类型 (0, 1, or 2)
baos.write((byte) ((format >> 8) & 0xFF));
baos.write((byte) (format & 0xFF));
// 音轨数
baos.write((byte) ((numTracks >> 8) & 0xFF));
baos.write((byte) (numTracks & 0xFF));
// 时间分割 (ticks per quarter note)
baos.write((byte) ((division >> 8) & 0xFF));
baos.write((byte) (division & 0xFF));
}
// 写入可变长度值
private void writeVarLen(ByteArrayOutputStream baos, int value) throws IOException {
if (value < 0) {
value = 0;
}
int buffer = value & 0x7F;
while ((value >>= 7) > 0) {
buffer <<= 8;
buffer |= 0x80;
buffer += (value & 0x7F);
}
while (true) {
baos.write((byte) (buffer & 0xFF));
if ((buffer & 0x80) != 0) {
buffer >>= 8;
} else {
break;
}
}
}
// 整数转字节数组
private byte[] intToBytes(int value, int length) {
ByteBuffer buffer = ByteBuffer.allocate(length);
buffer.order(ByteOrder.BIG_ENDIAN);
if (length == 2) {
buffer.putShort((short) value);
} else if (length == 4) {
buffer.putInt(value);
}
return buffer.array();
}
// 解析后的音乐数据类
private static class ParsedMusic {
int instrument;
int tempo;
String key;
List<NoteEvent> noteEvents = new ArrayList<>();
}
// 音符事件类
private static class NoteEvent {
List<String> notes;
int duration;
boolean isChord;
NoteEvent(String noteData, int baseDuration, boolean chord) {
this.isChord = chord;
this.duration = baseDuration;
if (noteData != null) {
notes = new ArrayList<>();
if (chord) {
// 和弦:将多个音符分开
for (char c : noteData.toCharArray()) {
if (c >= '1' && c <= '7') {
notes.add(String.valueOf(c));
}
}
} else {
// 单个音符
notes.add(noteData);
}
}
}
}
}
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp"
android:background="@android:color/darker_gray"
tools:context="com.example.miditest.MainActivity">
<!-- 上半部分:音轨文本框容器(所有文本框重叠在同一位置) -->
<FrameLayout
android:id="@+id/trackContainer"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginBottom="8dp">
<EditText
android:id="@+id/editTextTrack1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="音轨1: [音色,速度,调式]简谱内容"
android:inputType="textMultiLine"
android:minLines="3"
android:scrollbars="vertical"
android:visibility="visible"
android:text="[0,220,G大调]5-351'--76-1'-5---5-123-212--5-351'--76-1'-5---5-234--7.1--"/>
<!-- 其他15个音轨文本框将动态添加 -->
</FrameLayout>
<!-- 分隔线 -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@android:color/white"
android:layout_marginVertical="4dp"/>
<!-- 下半部分:控制按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="4">
<Button
android:id="@+id/buttonPlay"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="播放"
android:layout_margin="2dp"/>
<Button
android:id="@+id/buttonStop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="停止"
android:layout_margin="2dp"/>
<Button
android:id="@+id/buttonExport"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="导出MIDI"
android:layout_margin="2dp"/>
<CheckBox
android:id="@+id/checkBoxLoop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="循环播放"
android:layout_margin="2dp"/>
</LinearLayout>
<!-- 音轨选择按钮网格 -->
<GridView
android:id="@+id/trackButtonsGrid"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:numColumns="4"
android:verticalSpacing="4dp"
android:horizontalSpacing="4dp"
android:stretchMode="columnWidth"/>
</LinearLayout>
```
midi16JPBFQ\app\libs\MidiDriver-1.21.aar
通过网盘分享的文件:MidiDriver-1.21.aar
链接: https://pan.baidu.com/s/1a3wwy0H4OrKJdkTTKAsL9A 提取码: itkg
简谱播放器apk下载地址:
app-debug-midi16JPBFQ-ZXQMQZQ.apk
[https://download.csdn.net/download/qq_32257509/91870337](https://download.csdn.net/download/qq_32257509/91870337)
简谱播放器项目源代码下载地址:
midi16JPBFQ-ZXQMQZQ.zip
[https://download.csdn.net/download/qq_32257509/91870336](https://download.csdn.net/download/qq_32257509/91870336)
通过网盘分享的文件:app-debug-midi16JPBFQ-ZXQMQZQ.apk
链接: https://pan.baidu.com/s/14n0Yy3ebXceDBUijlFhw3g?pwd=yet8 提取码: yet8
通过网盘分享的文件:midi16JPBFQ-ZXQMQZQ.zip
链接: https://pan.baidu.com/s/1lUdS--ibTFUVgTzIFcFGzw?pwd=dumn 提取码: dumn