# -*- coding: utf-8 -*- MIDI转简谱 - 手机适配版 功能导入MIDI文件转换为简谱文本 适配手机屏幕小屏优化 import sys import os import re import threading from datetime import datetime # 尝试导入依赖 try: import mido except ImportError: print(请先安装 mido: pip install mido) sys.exit(1) try: import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext except ImportError: print(请先安装 tkinter (Python自带)) sys.exit(1) APP_NAME MIDI转简谱 APP_VERSION 1.0 APP_AUTHOR 叶海宁 APP_EMAIL 791687349qq.com class MidiToJianpuMobile: def __init__(self, root): self.root root self.root.title(f{APP_NAME} V{APP_VERSION}) # 手机适配设置 # 获取屏幕尺寸 screen_width root.winfo_screenwidth() screen_height root.winfo_screenheight() # 设置窗口大小手机适配全屏或接近全屏 if screen_width 800: # 手机屏幕 win_width screen_width - 20 win_height screen_height - 40 self.is_mobile True self.font_size 8 self.button_height 2 self.padding 5 else: # 平板或桌面 win_width min(600, screen_width - 100) win_height min(800, screen_height - 100) self.is_mobile False self.font_size 8 self.button_height 1 self.padding 10 self.root.geometry(f{win_width}x{win_height}) self.root.minsize(320, 480) # 颜色主题护眼模式 self.bg_color #f5f0e8 self.fg_color #2c3e50 self.accent_color #4CAF50 self.danger_color #e74c3c self.primary_color #3498db self.root.configure(bgself.bg_color) # 创建UI self.create_widgets() # 状态变量 self.current_file None self.converted_tracks [] # 状态栏 self.status_var tk.StringVar(value就绪 | 请导入MIDI文件) status_bar tk.Label( root, textvariableself.status_var, bd1, relieftk.SUNKEN, anchortk.W, font(微软雅黑, self.font_size - 2), bg#ecf0f1 ) status_bar.pack(sidetk.BOTTOM, filltk.X) def create_widgets(self): 创建UI组件 - 手机适配布局 # 主容器 main_frame tk.Frame(self.root, bgself.bg_color) main_frame.pack(filltk.BOTH, expandTrue, padxself.padding, padyself.padding) # 标题 title_font (微软雅黑, self.font_size 6, bold) title tk.Label( main_frame, textf {APP_NAME}, fonttitle_font, fgself.primary_color, bgself.bg_color ) title.pack(pady(10, 5)) subtitle tk.Label( main_frame, text导入MIDI文件 → 自动转换为简谱, font(微软雅黑, self.font_size - 2), fg#7f8c8d, bgself.bg_color ) subtitle.pack(pady(0, 15)) # 按钮区域横向布局 btn_frame tk.Frame(main_frame, bgself.bg_color) btn_frame.pack(filltk.X, pady5) # 导入按钮 self.import_btn tk.Button( btn_frame, text 导入MIDI, width10, commandself.import_midi_file, bgself.accent_color, fgwhite, font(微软雅黑, self.font_size), heightself.button_height, relieftk.RAISED ) self.import_btn.pack(sidetk.LEFT, filltk.X, expandTrue, padx2) # 复制按钮 self.copy_btn tk.Button( btn_frame, text 复制结果, width10, commandself.copy_result, bgself.primary_color, fgwhite, font(微软雅黑, self.font_size), heightself.button_height, statetk.DISABLED, relieftk.RAISED ) self.copy_btn.pack(sidetk.LEFT, filltk.X, expandTrue, padx2) # 清空按钮 self.clear_btn tk.Button( btn_frame, text️ 清空, width10, commandself.clear_result, bgself.danger_color, fgwhite, font(微软雅黑, self.font_size), heightself.button_height, relieftk.RAISED ) self.clear_btn.pack(sidetk.LEFT, filltk.X, expandTrue, padx2) # 音轨选择区域 track_frame tk.Frame(main_frame, bgself.bg_color) track_frame.pack(filltk.X, pady5) tk.Label( track_frame, text选择音轨:, font(微软雅黑, self.font_size), bgself.bg_color ).pack(sidetk.LEFT, padx2) self.track_var tk.StringVar(value全部) self.track_combo ttk.Combobox( track_frame, textvariableself.track_var, values[全部], statereadonly, font(微软雅黑, self.font_size), width15 if self.is_mobile else 20 ) self.track_combo.pack(sidetk.LEFT, filltk.X, expandTrue, padx2) self.track_combo.bind(ComboboxSelected, self.on_track_selected) # 结果显示区域 result_frame tk.Frame(main_frame, bgself.bg_color) result_frame.pack(filltk.BOTH, expandTrue, pady5) # 标签 label_frame tk.Frame(result_frame, bgself.bg_color) label_frame.pack(filltk.X) self.info_label tk.Label( label_frame, text 请导入MIDI文件查看转换结果, font(微软雅黑, self.font_size - 1), fg#7f8c8d, bgself.bg_color ) self.info_label.pack(sidetk.LEFT) # 字符计数 self.count_label tk.Label( label_frame, text, font(微软雅黑, self.font_size - 2), fg#95a5a6, bgself.bg_color ) self.count_label.pack(sidetk.RIGHT) # 文本框带滚动条 self.result_text scrolledtext.ScrolledText( result_frame, wraptk.WORD, font(Consolas, self.font_size), bgwhite, fgself.fg_color, relieftk.FLAT, bd2, highlightthickness1, highlightcolorself.primary_color ) self.result_text.pack(filltk.BOTH, expandTrue) # 底部信息 footer_frame tk.Frame(main_frame, bgself.bg_color) footer_frame.pack(filltk.X, pady5) footer_text f{APP_NAME} v{APP_VERSION} | 支持多音轨MIDI | 自动识别BPM tk.Label( footer_frame, textfooter_text, font(微软雅黑, self.font_size - 3), fg#bdc3c7, bgself.bg_color ).pack() def update_status(self, message, is_errorFalse): 更新状态栏 if is_error: self.status_var.set(f❌ {message}) else: self.status_var.set(f✅ {message}) self.root.update_idletasks() def import_midi_file(self): 导入MIDI文件 # 手机端使用文件选择器 midi_file filedialog.askopenfilename( title选择MIDI文件, filetypes[ (MIDI文件, *.mid), (MIDI文件, *.midi), (所有文件, *.*) ] ) if not midi_file: return self.current_file midi_file self.update_status(f正在解析: {os.path.basename(midi_file)}) # 在新线程中处理避免界面卡顿 threading.Thread(targetself._process_midi, args(midi_file,), daemonTrue).start() def _process_midi(self, midi_file): 处理MIDI文件在后台线程运行 try: tracks self.midi_to_jianpu_multi_track(midi_file) if not tracks: self.root.after(0, lambda: self._show_error(MIDI文件中未找到音符)) return self.converted_tracks tracks # 更新音轨选择下拉框 track_names [全部] [f音轨{i 1}: {t[name]} for i, t in enumerate(tracks)] self.root.after(0, lambda: self._update_track_combo(track_names)) # 显示全部音轨结果 self.root.after(0, lambda: self._display_all_tracks()) # 启用复制按钮 self.root.after(0, lambda: self.copy_btn.config(statetk.NORMAL)) self.root.after(0, lambda: self.update_status( f完成! 共 {len(tracks)} 个音轨 | {os.path.basename(midi_file)} )) except Exception as e: self.root.after(0, lambda: self._show_error(f转换失败: {str(e)})) def _show_error(self, message): 显示错误信息 self.update_status(message, is_errorTrue) messagebox.showerror(错误, message) def _update_track_combo(self, track_names): 更新音轨下拉框 self.track_combo[values] track_names self.track_var.set(全部) def _display_all_tracks(self): 显示所有音轨的简谱 if not self.converted_tracks: return result_parts [] total_notes 0 for i, track in enumerate(self.converted_tracks): bpm track.get(bpm, 120) jianpu track[jianpu] total_notes len(jianpu) # result_parts.append(f【音轨 {i 1}: {track[name]}】) # result_parts.append(f速度: {bpm} BPM) result_parts.append([0,320,G大调]) # result_parts.append() # 格式化简谱每80字符换行 for j in range(0, len(jianpu), 80): result_parts.append(jianpu[j:j 80]) result_parts.append() # result_parts.append(- * 50) result_parts.append() full_text \n.join(result_parts) self.result_text.delete(1.0, tk.END) self.result_text.insert(1.0, full_text) # 更新信息 self.info_label.config(textf 共 {len(self.converted_tracks)} 个音轨) self.count_label.config(textf音符数: {total_notes}) def on_track_selected(self, event): 选择音轨时触发 selection self.track_var.get() if not self.converted_tracks: return if selection 全部: self._display_all_tracks() return # 解析音轨索引 try: # 格式: 音轨X: 名称 import re match re.search(r音轨(\d), selection) if match: idx int(match.group(1)) - 1 if 0 idx len(self.converted_tracks): self._display_single_track(idx) except: pass def _display_single_track(self, idx): 显示单个音轨 track self.converted_tracks[idx] bpm track.get(bpm, 120) jianpu track[jianpu] result_parts [] # result_parts.append(f【音轨 {idx 1}: {track[name]}】) # result_parts.append(f速度: {bpm} BPM) result_parts.append([0,320,G大调]) # result_parts.append() for j in range(0, len(jianpu), 80): result_parts.append(jianpu[j:j 80]) full_text \n.join(result_parts) self.result_text.delete(1.0, tk.END) self.result_text.insert(1.0, full_text) self.info_label.config(textf 当前: {track[name]}) self.count_label.config(textf音符数: {len(jianpu)}) def copy_result(self): 复制结果到剪贴板 content self.result_text.get(1.0, tk.END).strip() if content: self.root.clipboard_clear() self.root.clipboard_append(content) self.update_status(已复制到剪贴板) # 手机端显示提示 messagebox.showinfo(成功, 简谱内容已复制到剪贴板) def clear_result(self): 清空结果 self.result_text.delete(1.0, tk.END) self.converted_tracks [] self.track_combo[values] [全部] self.track_var.set(全部) self.copy_btn.config(statetk.DISABLED) self.info_label.config(text 请导入MIDI文件查看转换结果) self.count_label.config(text) self.update_status(已清空) # MIDI转换核心代码 def midi_to_jianpu_multi_track(self, midi_file_path): 将MIDI文件转换为多条简谱每个轨道独立 支持升号 # 音符 # 完整音符映射包含升号 # note_map { # 中音区 (C4-B4) 60: 1, 61: 1#, 62: 2, 63: 2#, 64: 3, 65: 4, 66: 4#, 67: 5, 68: 5#, 69: 6, 70: 6#, 71: 7, # 高八度 (C5-B5) 72: 1, 73: 1#, 74: 2, 75: 2#, 76: 3, 77: 4, 78: 4#, 79: 5, 80: 5#, 81: 6, 82: 6#, 83: 7, # 高两个八度 (C6-B6) 84: 1, 85: 1#, 86: 2, 87: 2#, 88: 3, 89: 4, 90: 4#, 91: 5, 92: 5#, 93: 6, 94: 6#, 95: 7, # 高三个八度 (C7-B7) 96: 1, 97: 1#, 98: 2, 99: 2#, 100: 3, 101: 4, 102: 4#, 103: 5, 104: 5#, 105: 6, 106: 6#, 107: 7, # 低八度 (C3-B3) 48: 1., 49: 1#., 50: 2., 51: 2#., 52: 3., 53: 4., 54: 4#., 55: 5., 56: 5#., 57: 6., 58: 6#., 59: 7., # 低两个八度 (C2-B2) 36: 1.., 37: 1#.., 38: 2.., 39: 2#.., 40: 3.., 41: 4.., 42: 4#.., 43: 5.., 44: 5#.., 45: 6.., 46: 6#.., 47: 7.., # 低三个八度 (C1-B1) 24: 1..., 25: 1#..., 26: 2..., 27: 2#..., 28: 3..., 29: 4..., 30: 4#..., 31: 5..., 32: 5#..., 33: 6..., 34: 6#..., 35: 7..., } try: mid mido.MidiFile(midi_file_path) ticks_per_beat mid.ticks_per_beat tracks_result [] global_tempo 500000 for track_idx, track in enumerate(mid.tracks): active_notes {} note_events [] current_tempo global_tempo track_name f音轨{track_idx 1} abs_time 0 for msg in track: abs_time msg.time if msg.is_meta: if msg.type set_tempo: current_tempo msg.tempo global_tempo current_tempo elif msg.type track_name: track_name msg.name continue if msg.type note_on and msg.velocity 0: active_notes[msg.note] abs_time elif msg.type note_off or (msg.type note_on and msg.velocity 0): if msg.note in active_notes: duration abs_time - active_notes[msg.note] note_events.append((active_notes[msg.note], duration, msg.note)) del active_notes[msg.note] if not note_events: continue note_events.sort(keylambda x: x[0]) quarter_ticks ticks_per_beat score_parts [] for start_time, duration, note in note_events: note_str note_map.get(note, ) if not note_str: continue beat_ratio duration / quarter_ticks duration_symbol self._get_duration_symbol(beat_ratio) score_parts.append(note_str) if duration_symbol: score_parts.append(duration_symbol) bpm int(mido.tempo2bpm(current_tempo)) jianpu_text .join(score_parts) # 限制长度 if len(jianpu_text) 5000: jianpu_text jianpu_text[:5000] tracks_result.append({ name: track_name, bpm: bpm, jianpu: jianpu_text, track_index: track_idx }) return tracks_result if tracks_result else None except Exception as e: print(fMIDI导入错误: {e}) return None def _get_duration_symbol(self, beat_ratio): 根据拍数比例返回时值符号 beats round(beat_ratio * 4) / 4 if beats 4.0: return ---- elif beats 3.5: return --- elif beats 3.0: return --- elif beats 2.5: return -- elif beats 2.0: return -- elif beats 1.75: return - elif beats 1.5: return - elif beats 1.25: return - elif beats 1.0: return else: return def main(): root tk.Tk() # 设置应用图标如果有的话 try: root.iconbitmap(icon.ico) except: pass app MidiToJianpuMobile(root) root.mainloop() if __name__ __main__: main()