import mido
import time
import re
import tkinter as tk
from tkinter import messagebox
from threading import Thread, Event
stop_event = Event()
pause_event = Event()
output_port = None
MODE_MAP = {
1: "C大调",
2: "G大调",
3: "D大调",
4: "A大调",
5: "E大调",
6: "B大调",
7: "F大调",
8: "c小调",
9: "g小调",
10: "d小调",
11: "a小调",
12: "e小调",
13: "b小调",
14: "f小调"
}
def play_part(score, channel_offset):
global output_port
try:
first_line = score.split('\n')[0]
parts = first_line.split(',')
if len(parts) != 4:
raise IndexError("简谱第一行没有正确分隔乐器、音量、速度和调式编号信息")
instrument_part = parts[0]
volume_part = parts[1]
speed_part = parts[2]
mode_part = parts[3]
instrument_info = instrument_part.replace(':', ': ').split(': ')
volume_info = volume_part.replace(':', ': ').split(': ')
speed_info = speed_part.replace(':', ': ').split(': ')
if len(instrument_info) != 2 or len(volume_info) != 2 or len(speed_info) != 2:
raise IndexError("乐器、音量或速度信息格式错误,缺少冒号和空格")
instrument_list = instrument_info[1].strip('[]').split(', ')
volume_list = volume_info[1].strip('[]').split(', ')
speed = int(speed_info[1].strip('[]'))
mode_number = int(mode_part.strip())
instruments = [int(i) for i in instrument_list]
volumes = [int(v) for v in volume_list]
for i, instrument in enumerate(instruments):
msg = mido.Message('program_change', program=instrument, channel=i + channel_offset)
try:
output_port.send(msg)
except Exception as e:
print(f"发送 program_change 消息时出错 (乐器 {instrument}): {e}")
msg = mido.Message('control_change', control=7, value=volumes[i], channel=i + channel_offset)
try:
output_port.send(msg)
except Exception as e:
print(f"发送 control_change 消息时出错 (乐器 {instrument}): {e}")
note_str = score.split('\n')[1].strip()
notes = note_str.split(',')
for note in notes:
if note.strip() == "":
continue
if stop_event.is_set():
break
while pause_event.is_set():
time.sleep(0.1)
pitch, duration = parse_note(note)
if pitch is None:
print(f"无法解析音符 {note},跳过此音符")
continue
transposed_note = transpose_note(pitch, mode_number)
midi_note = convert_note(transposed_note)
if midi_note is not None:
msg = mido.Message('note_on', note=midi_note, velocity=64, channel=channel_offset)
try:
output_port.send(msg)
except Exception as e:
print(f"发送 note_on 消息时出错 (音符 {midi_note}): {e}")
adjusted_duration = duration * (60 / speed)
elapsed_time = 0
while elapsed_time < adjusted_duration:
if stop_event.is_set():
break
while pause_event.is_set():
time.sleep(0.1)
time.sleep(0.1)
elapsed_time += 0.1
msg = mido.Message('note_off', note=midi_note, velocity=0, channel=channel_offset)
try:
output_port.send(msg)
except Exception as e:
print(f"发送 note_off 消息时出错 (音符 {midi_note}): {e}")
except IndexError as e:
print(f"简谱字符串格式错误: {e},请检查乐器、音量、速度和调式编号信息的格式。")
except ValueError as e:
print(f"乐器编号、音量值、速度值或调式编号不是有效的整数,请检查输入: {e}")
def parse_note(note):
pitch_pattern = r'^[+#!-]?[0-7]'
pitch_match = re.search(pitch_pattern, note)
pitch = pitch_match.group(0) if pitch_match else None
duration_part = note[len(pitch):] if pitch else note
slashes = duration_part.count('/')
base_duration = 1 / (2 ** slashes)
dashes = duration_part.count('-')
duration = base_duration * (2 ** dashes)
dots = duration_part.count('.')
if dots == 1:
duration *= 1.5
elif dots == 2:
duration *= 1.75
return pitch, duration
def convert_note(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 transpose_note(pitch, mode_number):
if mode_number in [2, 3, 4, 5, 6]:
if pitch in ["1", "2", "3", "5", "6"]:
pitch = f"#{pitch}"
elif mode_number in [7, 8, 9, 10, 11, 12, 13, 14]:
if pitch in ["4", "7"]:
pitch = f"!{pitch}"
return pitch
def start_play():
global stop_event, pause_event, output_port
stop_event.clear()
pause_event.clear()
scores = [text_box.get("1.0", tk.END).strip() for text_box in text_boxes]
valid_scores = [(score, i) for i, score in enumerate(scores) if score]
try:
output_port = mido.open_output()
except Exception as e:
return stop_play()
threads = []
for score, channel_offset in valid_scores:
thread = Thread(target=play_part, args=(score, channel_offset))
threads.append(thread)
thread.start()
def pause_play():
global pause_event
if pause_event.is_set():
pause_event.clear()
pause_button.config(text="暂停")
else:
pause_event.set()
pause_button.config(text="继续")
def stop_play():
global stop_event, output_port
stop_event.set()
if output_port:
output_port.close()
def show_text_box(index):
for i, box in enumerate(text_boxes):
if i == index:
box.place(x=fixed_x, y=fixed_y, width=text_box_width, height=text_box_height)
buttons[i].config(bg="lightblue")
else:
box.place_forget()
buttons[i].config(bg="SystemButtonFace")
root = tk.Tk()
root.title("简谱播放器")
root.geometry("800x800")
text_boxes = []
buttons = []
def show_textbox_menu(event):
menu = tk.Menu(root, tearoff=0)
menu.add_command(label="剪切", command=lambda: event.widget.event_generate("<>"))
menu.add_command(label="复制", command=lambda: event.widget.event_generate("<>"))
menu.add_command(label="粘贴", command=lambda: event.widget.event_generate("<>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: event.widget.event_generate("<>"))
menu.post(event.x_root, event.y_root)
fixed_x = 10
fixed_y = 100
text_box_width = 600
text_box_height = 300
button_x = 630
button_y = 10
button_width = 100
button_height = 40
button_gap = 35
for i in range(16):
text_box = tk.Text(root, height=10, width=60)
text_boxes.append(text_box)
text_box.bind("", show_textbox_menu)
button = tk.Button(root, text=f"显示音轨 {i + 1}", command=lambda idx=i: show_text_box(idx))
buttons.append(button)
if i == 0:
text_box.place(x=fixed_x, y=fixed_y, width=text_box_width, height=text_box_height)
button.config(bg="lightblue")
button.place(x=button_x, y=button_y + i * button_gap, width=button_width, height=button_height)
example_score = "乐器:[0],音量:[100],速度:120,2\n1/,5/,5/,5/,4/,4/,5/-,4/,4/,2/,4/,1/,5/-,1/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/-,5/,6/,5/,4/,5/-,4/,5/,4/,4/,2/-,2/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/--,"
text_boxes[0].insert(tk.END, example_score)
score = "乐器:[10],音量:[100],速度:120,12\n1/,5/,5/,5/,4/,4/,5/-,4/,4/,2/,4/,1/,5/-,1/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/-,5/,6/,5/,4/,5/-,4/,5/,4/,4/,2/-,2/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/--,"
text_boxes[1].insert(tk.END, score)
score1 = "乐器:[110],音量:[100],速度:120,4\n1/,5/,5/,5/,4/,4/,5/-,4/,4/,2/,4/,1/,5/-,1/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/-,5/,6/,5/,4/,5/-,4/,5/,4/,4/,2/-,2/,4/,4/,6/,5/,3/,3/-,3/,5/,3/,2/,1/,2/--,"
text_boxes[2].insert(tk.END, score1)
control_button_x = 200
control_button_y = 30
control_button_width = 100
control_button_height = 40
play_button = tk.Button(root, text="播放", command=start_play)
play_button.place(x=control_button_x, y=control_button_y, width=control_button_width, height=control_button_height)
pause_button = tk.Button(root, text="暂停", command=pause_play)
pause_button.place(x=control_button_x + control_button_width + 10, y=control_button_y, width=control_button_width, height=control_button_height)
stop_button = tk.Button(root, text="停止", command=stop_play)
stop_button.place(x=control_button_x + 2 * (control_button_width + 10), y=control_button_y, width=control_button_width, height=control_button_height)
root.mainloop()