手机端python语言mid转换成数字简谱软件代码QZQ-2026-7-25
📅 2026/7/25 23:19:51
👁️ 阅读次数
📝 编程学习
# -*- 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 = "791687349@qq.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(bg=self.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, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W, font=("微软雅黑", self.font_size - 2), bg="#ecf0f1" ) status_bar.pack(side=tk.BOTTOM, fill=tk.X) def create_widgets(self): """创建UI组件 - 手机适配布局""" # ========== 主容器 ========== main_frame = tk.Frame(self.root, bg=self.bg_color) main_frame.pack(fill=tk.BOTH, expand=True, padx=self.padding, pady=self.padding) # ========== 标题 ========== title_font = ("微软雅黑", self.font_size + 6, "bold") title = tk.Label( main_frame, text=f"🎵 {APP_NAME}", font=title_font, fg=self.primary_color, bg=self.bg_color ) title.pack(pady=(10, 5)) subtitle = tk.Label( main_frame, text="导入MIDI文件 → 自动转换为简谱", font=("微软雅黑", self.font_size - 2), fg="#7f8c8d", bg=self.bg_color ) subtitle.pack(pady=(0, 15)) # ========== 按钮区域(横向布局) ========== btn_frame = tk.Frame(main_frame, bg=self.bg_color) btn_frame.pack(fill=tk.X, pady=5) # 导入按钮 self.import_btn = tk.Button( btn_frame, text="📂 导入MIDI", width=10, command=self.import_midi_file, bg=self.accent_color, fg="white", font=("微软雅黑", self.font_size), height=self.button_height, relief=tk.RAISED ) self.import_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2) # 复制按钮 self.copy_btn = tk.Button( btn_frame, text="📋 复制结果", width=10, command=self.copy_result, bg=self.primary_color, fg="white", font=("微软雅黑", self.font_size), height=self.button_height, state=tk.DISABLED, relief=tk.RAISED ) self.copy_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2) # 清空按钮 self.clear_btn = tk.Button( btn_frame, text="🗑️ 清空", width=10, command=self.clear_result, bg=self.danger_color, fg="white", font=("微软雅黑", self.font_size), height=self.button_height, relief=tk.RAISED ) self.clear_btn.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2) # ========== 音轨选择区域 ========== track_frame = tk.Frame(main_frame, bg=self.bg_color) track_frame.pack(fill=tk.X, pady=5) tk.Label( track_frame, text="选择音轨:", font=("微软雅黑", self.font_size), bg=self.bg_color ).pack(side=tk.LEFT, padx=2) self.track_var = tk.StringVar(value="全部") self.track_combo = ttk.Combobox( track_frame, textvariable=self.track_var, values=["全部"], state="readonly", font=("微软雅黑", self.font_size), width=15 if self.is_mobile else 20 ) self.track_combo.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2) self.track_combo.bind("<<ComboboxSelected>>", self.on_track_selected) # ========== 结果显示区域 ========== result_frame = tk.Frame(main_frame, bg=self.bg_color) result_frame.pack(fill=tk.BOTH, expand=True, pady=5) # 标签 label_frame = tk.Frame(result_frame, bg=self.bg_color) label_frame.pack(fill=tk.X) self.info_label = tk.Label( label_frame, text="💡 请导入MIDI文件查看转换结果", font=("微软雅黑", self.font_size - 1), fg="#7f8c8d", bg=self.bg_color ) self.info_label.pack(side=tk.LEFT) # 字符计数 self.count_label = tk.Label( label_frame, text="", font=("微软雅黑", self.font_size - 2), fg="#95a5a6", bg=self.bg_color ) self.count_label.pack(side=tk.RIGHT) # 文本框(带滚动条) self.result_text = scrolledtext.ScrolledText( result_frame, wrap=tk.WORD, font=("Consolas", self.font_size), bg="white", fg=self.fg_color, relief=tk.FLAT, bd=2, highlightthickness=1, highlightcolor=self.primary_color ) self.result_text.pack(fill=tk.BOTH, expand=True) # ========== 底部信息 ========== footer_frame = tk.Frame(main_frame, bg=self.bg_color) footer_frame.pack(fill=tk.X, pady=5) footer_text = f"{APP_NAME} v{APP_VERSION} | 支持多音轨MIDI | 自动识别BPM" tk.Label( footer_frame, text=footer_text, font=("微软雅黑", self.font_size - 3), fg="#bdc3c7", bg=self.bg_color ).pack() def update_status(self, message, is_error=False): """更新状态栏""" 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(target=self._process_midi, args=(midi_file,), daemon=True).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(state=tk.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_error=True) 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(text=f"🎵 共 {len(self.converted_tracks)} 个音轨") self.count_label.config(text=f"音符数: {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(text=f"🎵 当前: {track['name']}") self.count_label.config(text=f"音符数: {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(state=tk.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(key=lambda 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(f"MIDI导入错误: {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()
编程学习
技术分享
实战经验