#pip install mido
#pip install python-rtmidi
#pip install pyinstaller
#pyinstaller --onefile --hidden-import=mido.backends.rtmidi main.py
import mido
import time
import re
import tkinter as tk
from tkinter import messagebox
from threading import Thread, Event
import random
# 全局变量用于控制播放
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=17, 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:
# messagebox.showerror("错误", f"无法打开 MIDI 输出端口: {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()
output_port = None # 将 output_port 设置为 None,防止重复关闭
# 可以在这里添加其他需要的操作,例如重置 GUI 状态等
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()