```python
# -*- coding: utf-8 -*-
#需要先安装2个模块
# pip install mido
# pip install python-rtmidi
#pip install pyinstaller
#pyinstaller --onefile --icon=1.ico --hidden-import=mido.backends.rtmidi main.py
import os
import mido
from mido import Message
import time
import tkinter as tk
from tkinter import filedialog, messagebox
import re
import random
import threading
class SimpleScoreEditor:
def __init__(self, root):
self.root = root
self.root.title("MIDI简谱编辑器 V1.1")
self.root.geometry("900x800")
# 标题标签
self.title_label = tk.Label(root, text="MIDI简谱编辑器1.1", height=2)
self.title_label.pack()
# MIDI状态标签
self.midi_status_label = tk.Label(root, text="MIDI状态: 未连接", fg="red")
self.midi_status_label.place(x=5, y=340, width=500, height=50)
# 循环播放复选框
self.loop_var = tk.BooleanVar()
self.loop_checkbox = tk.Checkbutton(root, text="循环播放", variable=self.loop_var)
self.loop_checkbox.place(x=690, y=220, width=100, height=30)
# 只打开一次MIDI输出端口
self.output_port = None
try:
output_names = mido.get_output_names()
print(f"可用的MIDI输出设备: {output_names}")
if output_names:
self.output_port = mido.open_output(output_names[0])
self.midi_status_label.config(text=f"MIDI状态: 已连接到 {output_names[0]}", fg="green")
print(f"成功打开MIDI输出设备: {output_names[0]}")
else:
raise mido.NoOutputError("没有找到可用的MIDI输出设备")
except (mido.NoOutputError, OSError) as e:
print(f"无法打开MIDI输出端口: {e}")
self.midi_status_label.config(text=f"MIDI状态: 未连接 - {str(e)}", fg="red")
class DummyPort:
def send(self, message):
pass
def close(self):
pass
self.output_port = DummyPort()
# 为每个音轨分配独立的MIDI通道
self.track_channels = {}
for i in range(16):
self.track_channels[i] = i
# 添加线程锁
self.midi_lock = threading.Lock()
# 创建清空所有文本框的按钮
self.clear_button = tk.Button(root, text="清空所有", command=self.clear_all_textboxes)
self.clear_button.pack(pady=20)
self.clear_button.place(x=750, y=300, width=100, height=50)
# 同时播放所有有内容音轨的按钮
self.play_all_button = tk.Button(root, text="播放全部音轨", command=self.play_all_scores)
self.play_all_button.pack(pady=10)
self.play_all_button.place(x=690, y=80, width=100, height=30)
# 暂停按钮
self.pause_button = tk.Button(root, text="暂停", command=self.pause_playback, state=tk.DISABLED)
self.pause_button.pack(pady=10)
self.pause_button.place(x=800, y=130, width=50, height=30)
# 停止按钮
self.stop_button = tk.Button(root, text="停止", command=self.stop_playback, state=tk.DISABLED)
self.stop_button.pack(pady=10)
self.stop_button.place(x=800, y=180, width=50, height=30)
# 导出按钮
self.export_button = tk.Button(root, text="导出", command=self.export_midi)
self.export_button.pack(pady=10)
self.export_button.place(x=800, y=80, width=50, height=30)
# 创建生成随机音符的按钮
self.generate_button = tk.Button(root, text="随机谱曲音轨3", command=self.generate_random_notes)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=110, width=100, height=30)
# 创建生成随机音符的按钮
self.generate_button = tk.Button(root, text="随机谱曲音轨4", command=self.generate_random_notes1)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=140, width=100, height=30)
# 创建生成随机音符的按钮
self.generate_button = tk.Button(root, text="随机谱曲音轨5", command=self.generate_random_notes2)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=170, width=100, height=30)
self.output_port = None
try:
self.output_port = mido.open_output()
except (mido.NoOutputError, OSError):
print("无法打开MIDI输出端口")
# 播放控制标志
self.playing = False
self.paused = False
self.play_threads = []
# 文本框布局参数
self.textbox_x_start = -400 # 修改起始x坐标
self.textbox_y_start = -250 # 修改起始y坐标
self.textbox_width = 500 # 修改宽度
self.textbox_height = 300 # 修改高度
# 添加16个按钮和对应的16个文本框
self.textboxes = []
self.buttons = []
button_x = 630
button_y = 10
# 修改:为每个音轨分配独立的MIDI通道(0-15)
# self.output_port = mido.open_output()
self.track_channels = {} # 存储每个音轨对应的MIDI通道
for i in range(16):
self.track_channels[i] = i
textbox_x = self.textbox_x_start + (i % 4) * (self.textbox_width + 10)
textbox_y = self.textbox_y_start + (i // 4) * (self.textbox_height + 10)
textbox = tk.Text(root, height=10, width=60)
textbox.place(x=textbox_x, y=textbox_y, width=self.textbox_width, height=self.textbox_height)
textbox.place_forget()
textbox.bind("<Button-3>", lambda event, idx=i: self.show_textbox_menu(event, idx))
self.textboxes.append(textbox)
button = tk.Button(root, text=f"音轨 {i + 1}", command=lambda idx=i: self.show_textbox(idx))
button.place(x=button_x, y=button_y)
self.buttons.append(button)
button_y += 30
# 设置第一个音轨文本框的默认简谱内容(包含多级高低音示例)
default_score = """[0,80,G大调]5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------"""
self.textboxes[0].insert(tk.END, default_score)
# 设置第一个音轨文本框的默认简谱内容(包含多级高低音示例)
default_score = """[10,100,G大调]5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------"""
self.textboxes[8].insert(tk.END, default_score)
# 设置第一个音轨文本框的默认简谱内容(包含多级高低音示例)
default_score = """[40,140,G大调]5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------"""
self.textboxes[14].insert(tk.END, default_score)
# 默认显示第一个音轨文本框
self.show_textbox(0)
# 定义音符列表
# 定义音符列表
z = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "1", "2", "3", "4",
"5", "6", "7", "1*", "2*", "3*", "4*", "5*", "6*", "7*", "1**", "2**", "3**", "4**", "5**", "6**", "7**"]
# 定义音符列表
a = ["", "-", "--"]
# 定义生成随机音符的函数
def generate_random_notes(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z) + random.choice(self.a)
random_notes.append(random_note)
# 清空文本框3并插入随机生成的音符
self.textboxes[2].delete("1.0", tk.END) # 清空文本框3
self.textboxes[2].insert(tk.END, "[0,120,G大调]----" + "".join(random_notes)) # 将随机音符插入文本框3
# 定义音符列表
# 定义音符列表
z1 = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "1", "2", "3", "4",
"5", "6", "7", "1*", "2*", "3*", "4*", "5*", "6*", "7*", "1**", "2**", "3**", "4**", "5**", "6**", "7**"]
# 定义音符列表
a1 = ["", "-", "--"]
# 定义生成随机音符的函数
def generate_random_notes1(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z1) + random.choice(self.a1)
random_notes.append(random_note)
# 清空文本框3并插入随机生成的音符
self.textboxes[3].delete("1.0", tk.END) # 清空文本框4
self.textboxes[3].insert(tk.END, "[10,120,G大调]----" + "".join(random_notes)) # 将随机音符插入文本框3
# 定义音符列表
# 定义音符列表
z2 = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "1", "2", "3", "4",
"5", "6", "7", "1*", "2*", "3*", "4*", "5*", "6*", "7*", "1**", "2**", "3**", "4**", "5**", "6**", "7**"]
# 定义音符列表
a2 = ["", "-", "--"]
# 定义生成随机音符的函数
def generate_random_notes2(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z2) + random.choice(self.a2)
random_notes.append(random_note)
# 清空文本框3并插入随机生成的音符
self.textboxes[4].delete("1.0", tk.END) # 清空文本框5
self.textboxes[4].insert(tk.END, "[0,120,C大调]----" + "".join(random_notes)) # 将随机音符插入文本框3
# 定义清空所有文本框的函数
def clear_all_textboxes(self):
for text_box in self.textboxes:
text_box.delete("1.0", tk.END) # 清空文本框内容
def play_all_scores(self):
self.playing = True
self.paused = False
self.pause_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.NORMAL)
self.play_threads = []
# 遍历所有文本框
for i, textbox in enumerate(self.textboxes):
score = textbox.get("1.0", tk.END).strip()
if score:
instrument_number, bpm, mode, score_content = self.parse_score_header(score)
# 将 BPM 转换为四分音符时值(秒)
quarter_note_duration = 60 / bpm
# 获取拍子信息
beats_per_measure, beat_type = 4, 4
# 设置音色到对应通道
self.set_instrument(instrument_number, channel=self.track_channels[i])
# 设置调式
transpose = self.get_transpose(mode)
thread = threading.Thread(target=self._play_single_score, args=(
score_content, quarter_note_duration, transpose, beats_per_measure, beat_type,
self.track_channels[i], self.loop_var.get()))
self.play_threads.append(thread)
thread.start()
def _play_helper(self, score_text_input):
if self.output_port:
self.playing = True
self.paused = False
self.pause_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.NORMAL)
score = score_text_input.get("1.0", tk.END).strip()
instrument_number, bpm, mode, score_content = self.parse_score_header(score)
# 将 BPM 转换为四分音符时值(秒)
quarter_note_duration = 60 / bpm
# 获取拍子信息(这里假设默认 4/4,可根据需要扩展)
beats_per_measure, beat_type = 4, 4
# 设置音色
self.set_instrument(instrument_number)
# 设置调式
transpose = self.get_transpose(mode)
thread = threading.Thread(target=self._play_single_score, args=(
score_content, quarter_note_duration, transpose, beats_per_measure, beat_type, 0, self.loop_var.get()))
self.play_threads.append(thread)
thread.start()
def _play_single_score(self, score, quarter_note_duration, transpose, beats_per_measure, beat_type, channel,
loop=False):
while self.playing:
i = 0
beat_count = 0
while i < len(score) and self.playing:
while self.paused:
time.sleep(0.1)
note, duration = self.parse_note(score, i, quarter_note_duration)
if note is not None:
midi_note = self.note_to_midi(note)
if midi_note is not None:
midi_note += transpose
with self.midi_lock:
msg_on = Message('note_on', note=midi_note, velocity=127, channel=channel, time=0)
self.output_port.send(msg_on)
start_time = time.time()
while time.time() - start_time < duration and self.playing:
if self.paused:
time.sleep(0.1)
else:
time.sleep(0.01)
with self.midi_lock:
if self.playing:
msg_off = Message('note_off', note=midi_note, velocity=127, channel=channel, time=0)
self.output_port.send(msg_off)
beat_count += duration / quarter_note_duration
if beat_count >= beats_per_measure:
beat_count = 0
i += len(note) if note else 1
if not self.playing:
self._stop_all_notes()
self.playing = False
self.pause_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
# 如果不循环播放,则退出循环
if not loop:
break
def pause_playback(self):
if self.playing:
self.paused = not self.paused
if self.paused:
self.pause_button.config(text="继续")
else:
self.pause_button.config(text="暂停")
def stop_playback(self):
self.playing = False
self.paused = False
self.pause_button.config(state=tk.DISABLED, text="暂停")
self.stop_button.config(state=tk.DISABLED)
self._stop_all_notes()
def _stop_all_notes(self):
if self.output_port:
for channel in range(16):
for note in range(128):
msg_off = Message('note_off', note=note, velocity=64, channel=channel, time=0)
self.output_port.send(msg_off)
def parse_note(self, score, start_index, default_duration):
note = ""
duration = default_duration
i = start_index
# 解析音符部分
if i < len(score) and score[i].isdigit():
note = score[i]
i += 1
# 检查是否有低音或高音标记
if i < len(score) and score[i] == '.':
note += '.'
i += 1
elif i < len(score) and score[i] == '*':
note += '*'
i += 1
# 处理时值
dash_count = 0
while i < len(score) and score[i] == '-':
dash_count += 1
i += 1
if dash_count > 0:
duration *= (dash_count + 1)
return note, duration
def note_to_midi(self, note):
if not note:
return None
base_notes = {
"1": 60, "2": 62, "3": 64, "4": 65, "5": 67, "6": 69, "7": 71,
"0": None # 休止符
}
base_note = note[0]
if base_note not in base_notes:
return None
midi_note = base_notes[base_note]
# 处理低音和高音(支持多个.或*)
if len(note) > 1:
if '.' in note: # 低音(每个.降一个八度)
octave_shift = -12 * note.count('.')
midi_note += octave_shift
elif '*' in note: # 高音(每个*升一个八度)
octave_shift = 12 * note.count('*')
midi_note += octave_shift
# 确保音高在有效范围内(0-127)
if midi_note is not None:
midi_note = max(0, min(127, midi_note))
return midi_note
def show_textbox_menu(self, event, index):
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="剪切", command=lambda: self.textboxes[index].event_generate("<<Cut>>"))
menu.add_command(label="复制", command=lambda: self.textboxes[index].event_generate("<<Copy>>"))
menu.add_command(label="粘贴", command=lambda: self.textboxes[index].event_generate("<<Paste>>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: self.textboxes[index].event_generate("<<SelectAll>>"))
# 新增:全选复制(先选中所有文本,再触发复制)
menu.add_command(
label="全选复制",
command=lambda: (
self.textboxes[index].tag_add("sel", "1.0", "end"), # 全选文本
self.textboxes[index].event_generate("<<Copy>>") # 触发复制
)
)
# 新增:全选删除(直接删除所有文本)
menu.add_separator() # 分隔线优化菜单结构
menu.add_command(
label="全选删除",
command=lambda: self.textboxes[index].delete("1.0", "end")
)
menu.post(event.x_root, event.y_root)
def show_textbox(self, index):
for textbox in self.textboxes:
textbox.place_forget() # 隐藏所有文本框
self.textboxes[index].place(x=self.textbox_x_start + (self.textbox_width),
y=self.textbox_y_start + (self.textbox_height),
width=self.textbox_width, height=self.textbox_height)
def set_instrument(self, instrument_number, channel=0):
msg = Message('program_change', program=instrument_number, channel=channel, time=0)
if hasattr(self, 'output_port') and self.output_port:
self.output_port.send(msg)
def get_transpose(self, mode):
# 调式转换表
mode_transpose = {
"C大调": 0,
"G大调": 7,
"D大调": 2,
"A大调": 9,
"E大调": 4,
"B大调": 11,
"F大调": -5
}
return mode_transpose.get(mode, 0)
def parse_score_header(self, score):
match = re.match(r'\[(\d+),(\d+),([^]]+)\]', score)
if match:
instrument_number = int(match.group(1))
bpm = int(match.group(2))
mode = match.group(3)
score_content = score[match.end():].strip()
return instrument_number, bpm, mode, score_content
return 0, 120, "C大调", score
def export_midi(self):
file_path = filedialog.asksaveasfilename(defaultextension=".mid", filetypes=[("MIDI Files", "*.mid")])
if file_path:
try:
mid = mido.MidiFile()
for i, textbox in enumerate(self.textboxes):
score = textbox.get("1.0", tk.END).strip()
if score:
instrument_number, bpm, mode, score_content = self.parse_score_header(score)
track = mido.MidiTrack()
mid.tracks.append(track)
track.append(mido.MetaMessage('track_name', name=f"Track {i + 1}"))
tempo = mido.bpm2tempo(bpm)
track.append(mido.MetaMessage('set_tempo', tempo=tempo))
track.append(mido.MetaMessage('time_signature', numerator=4, denominator=4))
channel = i % 16
track.append(Message('program_change', program=instrument_number, channel=channel, time=0))
transpose = self.get_transpose(mode)
self._add_notes_to_track(track, score_content, transpose, bpm, channel)
mid.save(file_path)
messagebox.showinfo("成功", f"MIDI文件已保存到:\n{file_path}")
except Exception as e:
messagebox.showerror("错误", f"保存MIDI文件时出错:\n{str(e)}")
def _add_notes_to_track(self, track, score, transpose, bpm, channel):
quarter_note_duration = 60 / bpm
i = 0
current_time = 0 # 记录当前时间位置
while i < len(score):
note, duration = self.parse_note(score, i, quarter_note_duration)
if note is not None:
midi_note = self.note_to_midi(note)
if midi_note is not None:
# 根据调式进行转换
midi_note += transpose
# 计算音符持续时间(以MIDI时间单位表示)
note_duration = int(duration * 1000) # 转换为毫秒
# 发送音符开启消息,指定通道和时间
msg_on = Message('note_on', note=midi_note, velocity=127, channel=channel, time=0)
track.append(msg_on)
# 发送音符关闭消息,指定通道和时间间隔
msg_off = Message('note_off', note=midi_note, velocity=127, channel=channel, time=note_duration)
track.append(msg_off)
# 更新当前时间
current_time += note_duration
i += len(note) if note else 1
def _stop_all_notes(self):
if hasattr(self, 'output_port') and self.output_port:
for channel in range(16):
for note in range(128):
msg_off = Message('note_off', note=note, velocity=64, channel=channel, time=0)
self.output_port.send(msg_off)
if __name__ == "__main__":
root = tk.Tk()
app = SimpleScoreEditor(root)
root.mainloop()
```
源代码下载地址:[https://download.csdn.net/download/qq_32257509/91715334](https://download.csdn.net/download/qq_32257509/91715334)
软件项目下载地址:[https://download.csdn.net/download/qq_32257509/91715333](https://download.csdn.net/download/qq_32257509/91715333)
开源项目通过网盘分享的文件:MIDI简谱编辑器V1.1.zip
链接: https://pan.baidu.com/s/1pe8MbqpSgc8P_J64zRa_tw?pwd=m35g 提取码: m35g
开源项目通过网盘分享的文件:MIDI简谱编辑器V1.1软件.zip
链接: https://pan.baidu.com/s/1UCzky4ei9qAh8kSjknp2ow?pwd=spbh 提取码: spbh
开源项目通过网盘分享的文件:MIDI简谱编辑器 V1.1.exe
链接: https://pan.baidu.com/s/1-3uZyrImVQFYK49UqGBgmQ?pwd=9s3x 提取码: 9s3x
