python语言Xmido简谱播放器程序代码ZXQZQ7-2025-5-3

#pip install mido
#pip install python-rtmidi







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:
        # 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()


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

# 创建 16 个文本框和按钮
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()

你可能感兴趣的:(python,开发语言)