import os
import mido
from mido import Message
import time
import tkinter as tk
import threading
from tkinter import filedialog
class SimpleScoreEditor:
instrument_options = ["大钢琴(声学钢琴)", "明亮的钢琴", "电钢琴"]
mode_options = ["C大调", "G大调", "D大调", "A大调", "E大调", "B大调", "F大调"]
time_signature_options = ["2/4", "3/4", "4/4", "6/8"]
def __init__(self, root):
self.root = root
self.root.title("简易简谱编辑器")
self.root.geometry("900x800")
# 标题标签
self.title_label = tk.Label(root, text="简易简谱编辑器", height=2)
self.title_label.pack()
# 第一个简谱输入框
self.score_text_input1 = tk.Text(root, height=10)
self.score_text_input1.place(x=100, y=50, width=600, height=100)
self.score_text_input1.insert(tk.END, """在此输入简谱内容,如:-5/-,1/-,1/,1/-,3/,2/-,1/,2/-,3/,1/,1/-,1/,3/,5/,6/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,3/,1/,-6/,-6/,-5/,1/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,
6/,5/-,3/,3/-,5/-,6/--,
+1/,5/,3/,3/-,1/,2/,1/,2/-,
3/,1/-,-6/,-6/-,-5/,1/--,""")
self.score_text_input1.config(fg="gray")
self.score_text_input1.bind("<FocusIn>", self.clear_placeholder1)
self.score_text_input1.bind("<Button-3>", self.show_textbox_menu1)
self.score_text_input1.pack(pady=10)
# 第一个文本框显示/隐藏按钮
self.toggle_button1 = tk.Button(root, text="切换文本框1显示",
command=lambda: self.toggle_textbox(self.score_text_input1,
self.toggle_button1))
self.toggle_button1.pack(pady=5)
self.toggle_button1.place(x=700, y=50, width=100, height=30)
# 第一个音色选择下拉框
self.instrument_var1 = tk.StringVar(root)
self.instrument_var1.set("钢琴") # 默认音色
self.instrument_menu1 = tk.OptionMenu(root, self.instrument_var1, *self.instrument_options)
self.instrument_menu1.pack(pady=5)
self.instrument_menu1.place(x=300, y=150, width=100, height=50)
# 第一个调式选择下拉框
self.mode_var1 = tk.StringVar(root)
self.mode_var1.set("C大调") # 默认调式
self.mode_menu1 = tk.OptionMenu(root, self.mode_var1, *self.mode_options)
self.mode_menu1.pack(pady=5)
self.mode_menu1.place(x=450, y=150, width=100, height=50)
# 第二个简谱输入框
self.score_text_input2 = tk.Text(root, height=10)
self.score_text_input2.place(x=100, y=200, width=600, height=100)
self.score_text_input2.insert(tk.END, """在此输入简谱内容,如:-5/-,1/-,1/,1/-,3/,2/-,1/,2/-,3/,1/,1/-,1/,3/,5/,6/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,3/,1/,-6/,-6/,-5/,1/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,
6/,5/-,3/,3/-,5/-,6/--,
+1/,5/,3/,3/-,1/,2/,1/,2/-,
3/,1/-,-6/,-6/-,-5/,1/--,""")
self.score_text_input2.config(fg="gray")
self.score_text_input2.bind("<FocusIn>", self.clear_placeholder2)
self.score_text_input2.bind("<Button-3>", self.show_textbox_menu2)
self.score_text_input2.pack(pady=10)
# 第二个文本框显示/隐藏按钮
self.toggle_button2 = tk.Button(root, text="切换文本框2显示",
command=lambda: self.toggle_textbox(self.score_text_input2,
self.toggle_button2))
self.toggle_button2.pack(pady=5)
self.toggle_button2.place(x=700, y=200, width=100, height=30)
# 第二个音色选择下拉框
self.instrument_var2 = tk.StringVar(root)
self.instrument_var2.set("钢琴") # 默认音色
self.instrument_menu2 = tk.OptionMenu(root, self.instrument_var2, *self.instrument_options)
self.instrument_menu2.pack(pady=5)
self.instrument_menu2.place(x=300, y=300, width=100, height=50)
# 第二个调式选择下拉框
self.mode_var2 = tk.StringVar(root)
self.mode_var2.set("G大调") # 默认调式
self.mode_menu2 = tk.OptionMenu(root, self.mode_var2, *self.mode_options)
self.mode_menu2.pack(pady=5)
self.mode_menu2.place(x=450, y=300, width=100, height=50)
# 第三个简谱输入框
self.score_text_input3 = tk.Text(root, height=10)
self.score_text_input3.pack(pady=10)
self.score_text_input3.place(x=100, y=350, width=600, height=200)
self.score_text_input3.insert(tk.END, """在此输入简谱内容,如:-5/-,1/-,1/,1/-,3/,2/-,1/,2/-,3/,1/,1/-,1/,3/,5/,6/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,3/,1/,-6/,-6/,-5/,1/--,
6/,5/,3/,3/-,1/,2/,1/,2/--,
6/,5/-,3/,3/-,5/-,6/--,
+1/,5/,3/,3/-,1/,2/,1/,2/-,
3/,1/-,-6/,-6/-,-5/,1/--,""")
self.score_text_input3.config(fg="gray")
self.score_text_input3.bind("<FocusIn>", self.clear_placeholder3)
self.score_text_input3.bind("<Button-3>", self.show_textbox_menu3)
self.score_text_input3.pack(pady=10)
# 第三个文本框显示/隐藏按钮
self.toggle_button3 = tk.Button(root, text="切换文本框3显示",
command=lambda: self.toggle_textbox(self.score_text_input3,
self.toggle_button3))
self.toggle_button3.pack(pady=5)
self.toggle_button3.place(x=700, y=350, width=100, height=30)
# 第三个音色选择下拉框
self.instrument_var3 = tk.StringVar(root)
self.instrument_var3.set("钢琴") # 默认音色
self.instrument_menu3 = tk.OptionMenu(root, self.instrument_var3, *self.instrument_options)
self.instrument_menu3.pack(pady=5)
self.instrument_menu3.place(x=300, y=450, width=100, height=50)
# 第三个调式选择下拉框
self.mode_var3 = tk.StringVar(root)
self.mode_var3.set("G大调") # 默认调式
self.mode_menu3 = tk.OptionMenu(root, self.mode_var3, *self.mode_options)
self.mode_menu3.pack(pady=5)
self.mode_menu3.place(x=450, y=450, width=100, height=50)
# 速度输入框,现在是每分钟节拍数(BPM)
self.speed_label = tk.Label(root, text="输入播放速度(每分钟节拍数,BPM),默认 120 BPM")
self.speed_label.pack()
self.speed_label.place(x=500, y=500, width=300, height=40)
self.speed_input = tk.Entry(root)
self.speed_input.pack(pady=5)
self.speed_input.place(x=500, y=530, width=300, height=30)
# 拍子选择下拉框
self.time_signature_var = tk.StringVar(root)
self.time_signature_var.set("4/4") # 默认拍子
self.time_signature_menu = tk.OptionMenu(root, self.time_signature_var, *self.time_signature_options)
self.time_signature_menu.pack(pady=5)
self.time_signature_menu.place(x=550, y=150, width=100, height=50)
# 第二个拍子选择下拉框
self.time_signature_var1 = tk.StringVar(root)
self.time_signature_var1.set("4/4") # 默认拍子
self.time_signature_menu1 = tk.OptionMenu(root, self.time_signature_var1, *self.time_signature_options)
self.time_signature_menu1.pack(pady=5)
self.time_signature_menu1.place(x=550, y=300, width=100, height=50)
# 第三个拍子选择下拉框
self.time_signature_var2 = tk.StringVar(root)
self.time_signature_var2.set("4/4") # 默认拍子
self.time_signature_menu2 = tk.OptionMenu(root, self.time_signature_var2, *self.time_signature_options)
self.time_signature_menu2.pack(pady=5)
self.time_signature_menu2.place(x=550, y=450, width=100, height=50)
# 播放按钮
self.play_button1 = tk.Button(root, text="播放1", command=self.play_score)
self.play_button1.pack(pady=10)
self.play_button1.place(x=700, y=80, width=50, height=30)
# 播放按钮
self.play_button2 = tk.Button(root, text="播放2", command=self.play_score2)
self.play_button2.pack(pady=10)
self.play_button2.place(x=700, y=250, width=50, height=30)
# 播放按钮
self.play_button3 = tk.Button(root, text="播放3", command=self.play_score3)
self.play_button3.pack(pady=10)
self.play_button3.place(x=700, y=420, width=50, height=30)
# 同时播放三个编辑框简谱的按钮
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=700, y=160, 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=700, y=320, width=50, height=30)
# 初始化MIDI输出端口
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_thread = None
def play_score2(self):
self._play_helper(self.score_text_input2, self.instrument_var2, self.mode_var2, self.time_signature_var1)
def play_score(self):
self._play_helper(self.score_text_input1, self.instrument_var1, self.mode_var1, self.time_signature_var)
def play_score3(self):
self._play_helper(self.score_text_input3, self.instrument_var3, self.mode_var3, self.time_signature_var2)
def play_all_scores(self):
thread1 = threading.Thread(target=self.play_score)
thread2 = threading.Thread(target=self.play_score2)
thread3 = threading.Thread(target=self.play_score3)
thread1.start()
thread2.start()
thread3.start()
def _play_helper(self, score_text_input, instrument_var, mode_var, time_signature_var):
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()
try:
bpm = float(self.speed_input.get()) if self.speed_input.get() else 120
# 将 BPM 转换为四分音符时值(秒)
quarter_note_duration = 60 / bpm
except ValueError:
print("输入的播放速度不是有效的数字,使用默认值 120 BPM")
quarter_note_duration = 60 / 120
# 获取拍子信息
time_signature = time_signature_var.get()
beats_per_measure, beat_type = map(int, time_signature.split('/'))
# 设置音色
instrument_name = instrument_var.get()
instrument_number = self.get_instrument_number(instrument_name)
self.set_instrument(instrument_number)
# 设置调式
mode = mode_var.get()
transpose = self.get_transpose(mode)
self.play_thread = threading.Thread(target=self._play_single_score, args=(
score, quarter_note_duration, transpose, beats_per_measure, beat_type))
self.play_thread.start()
def _play_single_score(self, score, quarter_note_duration, transpose, beats_per_measure, beat_type):
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
# 发送音符开启消息
msg_on = Message('note_on', note=midi_note, velocity=64, time=0)
self.output_port.send(msg_on)
print(f"发送音符开启消息: {midi_note}")
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)
# 发送音符关闭消息
if self.playing:
msg_off = Message('note_off', note=midi_note, velocity=64, time=0)
self.output_port.send(msg_off)
print(f"发送音符关闭消息: {midi_note},持续时长: {duration} 秒")
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)
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 note in range(128):
msg_off = Message('note_off', note=note, velocity=64, time=0)
self.output_port.send(msg_off)
def parse_note(self, score, start_index, default_duration):
note = ""
duration = default_duration
i = start_index
# 解析音符部分(包含 + - #! 符号)
while i < len(score) and (score[i].isdigit() or score[i] in ('+', '-', '#', '!')):
note += score[i]
i += 1
# 处理斜杠,改变时值
slash_count = 0
while i < len(score) and score[i] == '/':
slash_count += 1
i += 1
if slash_count > 0:
duration /= 2 ** slash_count
# 处理延音线
dash_count = 0
while i < len(score) and score[i] == '-':
dash_count += 1
i += 1
if dash_count > 0:
duration *= (dash_count + 1)
# 处理附点音符
dot_count = 0
while i < len(score) and score[i] == '.':
dot_count += 1
i += 1
for _ in range(dot_count):
duration += duration / 2
# 跳过逗号
if i < len(score) and score[i] == ',':
i += 1
return note, duration
def note_to_midi(self, note):
base_notes = {
"1": 60, "2": 62, "3": 64, "4": 65, "5": 67, "6": 69, "7": 71,
"0": None # 表示休止符
}
modifier = 0
base_note_str = note
if note.startswith(('+', '-', '#', '!')):
if note.startswith('+'):
modifier = 12
base_note_str = note[1:]
elif note.startswith('-'):
modifier = -12
base_note_str = note[1:]
elif note.startswith('#'):
modifier = 1
base_note_str = note[1:]
elif note.startswith('!'):
modifier = -1
base_note_str = note[1:]
base_midi = base_notes.get(base_note_str)
if base_midi is not None:
return base_midi + modifier
return None
def show_textbox_menu1(self, event):
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="剪切", command=lambda: self.score_text_input1.event_generate("<<Cut>>"))
menu.add_command(label="复制", command=lambda: self.score_text_input1.event_generate("<<Copy>>"))
menu.add_command(label="粘贴", command=lambda: self.score_text_input1.event_generate("<<Paste>>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: self.score_text_input1.event_generate("<<SelectAll>>"))
menu.post(event.x_root, event.y_root)
def show_textbox_menu2(self, event):
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="剪切", command=lambda: self.score_text_input2.event_generate("<<Cut>>"))
menu.add_command(label="复制", command=lambda: self.score_text_input2.event_generate("<<Copy>>"))
menu.add_command(label="粘贴", command=lambda: self.score_text_input2.event_generate("<<Paste>>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: self.score_text_input2.event_generate("<<SelectAll>>"))
menu.post(event.x_root, event.y_root)
def show_textbox_menu3(self, event):
menu = tk.Menu(self.root, tearoff=0)
menu.add_command(label="剪切", command=lambda: self.score_text_input3.event_generate("<<Cut>>"))
menu.add_command(label="复制", command=lambda: self.score_text_input3.event_generate("<<Copy>>"))
menu.add_command(label="粘贴", command=lambda: self.score_text_input3.event_generate("<<Paste>>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: self.score_text_input3.event_generate("<<SelectAll>>"))
menu.post(event.x_root, event.y_root)
def clear_placeholder1(self, event):
if self.score_text_input1.cget("fg") == "gray":
self.score_text_input1.delete("1.0", tk.END)
self.score_text_input1.config(fg="black")
def clear_placeholder2(self, event):
if self.score_text_input2.cget("fg") == "gray":
self.score_text_input2.delete("1.0", tk.END)
self.score_text_input2.config(fg="black")
def clear_placeholder3(self, event):
if self.score_text_input3.cget("fg") == "gray":
self.score_text_input3.delete("1.0", tk.END)
self.score_text_input3.config(fg="black")
def toggle_textbox(self, textbox, button):
if textbox.winfo_ismapped():
textbox.pack_forget()
button.config(text=f"显示文本框{button.cget('text')[-1]}")
else:
textbox.pack()
button.config(text=f"切换文本框{button.cget('text')[-1]}显示")
def get_instrument_number(self, instrument_name):
# 这里简单映射,实际可能需要更复杂的映射
if instrument_name == "大钢琴(声学钢琴)":
return 0
elif instrument_name == "明亮的钢琴":
return 1
elif instrument_name == "电钢琴":
return 2
return 0
def set_instrument(self, instrument_number):
# 发送程序改变消息来设置乐器
msg = Message('program_change', program=instrument_number, time=0)
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 export_midi(self):
# 打开文件保存对话框
file_path = filedialog.asksaveasfilename(defaultextension=".mid", filetypes=[("MIDI Files", "*.mid")])
if file_path:
try:
# 创建一个新的 MIDI 文件
mid = mido.MidiFile()
# 获取速度
try:
bpm = float(self.speed_input.get()) if self.speed_input.get() else 120
except ValueError:
print("输入的播放速度不是有效的数字,使用默认值 120 BPM")
bpm = 120
tempo = mido.bpm2tempo(bpm)
# 处理第一个简谱输入框
score1 = self.score_text_input1.get("1.0", tk.END).strip()
track1 = mido.MidiTrack()
mid.tracks.append(track1)
# 设置速度
track1.append(mido.MetaMessage('set_tempo', tempo=tempo))
# 设置拍子
time_signature = self.time_signature_var.get()
numerator, denominator = map(int, time_signature.split('/'))
track1.append(mido.MetaMessage('time_signature', numerator=numerator, denominator=denominator))
# 设置音色
instrument_name1 = self.instrument_var1.get()
instrument_number1 = self.get_instrument_number(instrument_name1)
track1.append(Message('program_change', program=instrument_number1, time=0))
# 设置调式
mode1 = self.mode_var1.get()
transpose1 = self.get_transpose(mode1)
self._add_notes_to_track(track1, score1, transpose1, bpm)
# 处理第二个简谱输入框
score2 = self.score_text_input2.get("1.0", tk.END).strip()
track2 = mido.MidiTrack()
mid.tracks.append(track2)
# 设置速度
track2.append(mido.MetaMessage('set_tempo', tempo=tempo))
# 设置拍子
time_signature = self.time_signature_var1.get()
numerator, denominator = map(int, time_signature.split('/'))
track2.append(mido.MetaMessage('time_signature', numerator=numerator, denominator=denominator))
# 设置音色
instrument_name2 = self.instrument_var2.get()
instrument_number2 = self.get_instrument_number(instrument_name2)
track2.append(Message('program_change', program=instrument_number2, time=0))
# 设置调式
mode2 = self.mode_var2.get()
transpose2 = self.get_transpose(mode2)
self._add_notes_to_track(track2, score2, transpose2, bpm)
# 处理第三个简谱输入框
score3 = self.score_text_input3.get("1.0", tk.END).strip()
track3 = mido.MidiTrack()
mid.tracks.append(track3)
# 设置速度
track3.append(mido.MetaMessage('set_tempo', tempo=tempo))
# 设置拍子
time_signature = self.time_signature_var2.get()
numerator, denominator = map(int, time_signature.split('/'))
track3.append(mido.MetaMessage('time_signature', numerator=numerator, denominator=denominator))
# 设置音色
instrument_name3 = self.instrument_var3.get()
instrument_number3 = self.get_instrument_number(instrument_name3)
track3.append(Message('program_change', program=instrument_number3, time=0))
# 设置调式
mode3 = self.mode_var3.get()
transpose3 = self.get_transpose(mode3)
self._add_notes_to_track(track3, score3, transpose3, bpm)
# 保存 MIDI 文件
mid.save(file_path)
print(f"MIDI 文件已保存到 {file_path}")
except Exception as e:
print(f"保存 MIDI 文件时出错: {e}")
def _add_notes_to_track(self, track, score, transpose, bpm):
quarter_note_duration = 60 / bpm
i = 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
# 发送音符开启消息
msg_on = mido.Message('note_on', note=midi_note, velocity=64, time=0)
track.append(msg_on)
# 发送音符关闭消息
msg_off = mido.Message('note_off', note=midi_note, velocity=64, time=int(duration * 1000))
track.append(msg_off)
i += len(note) if note else 1
if __name__ == "__main__":
root = tk.Tk()
app = SimpleScoreEditor(root)
root.mainloop()
python语言mido简谱播放器3音轨程序代码3
于 2025-04-21 06:07:41 首次发布