python语言mido16音轨简谱播放器QZQ-2025-4-24

# -*- coding: utf-8 -*-
#需要先安装2个模块
# pip install mido
# pip install python-rtmidi
#pip install pyinstaller
#pyinstaller --onefile --hidden-import=mido.backends.rtmidi main.py




import os
import mido
from mido import Message
import time
import tkinter as tk
import threading
from tkinter import filedialog
import re


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.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)

        # 初始化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_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
        for i in range(16):
            # 计算文本框的位置和大小
            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,120,G大调]----+6/,+6/,+7/,+7/,++1/,++1/,+7/,+7/,+6/,+6/,+3/,+3/,+1/,+1/,6/,6/,+5/,+5/,+4/,+4/,+3/,+4/,+5/,+4/,+4/--,+4/,+4/,+5/,+5/,+6/,+6/,+7/,+7/,+5/,+5/,+2/,+2/,+4/,+4/,+3/,+3/,+2/,+2/,+4/,+3/--,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+4/,+3/,+4/,6/,+1/,+4/,+3/,+4/,+4/,+3/,+4/,#+4/,+5/,+5/,+6/,+5/,+6/,+3/--,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+4/,+3/,+4/,6/,+1/,+4/,+3/,+4/,+4/,+3/,+4/,+4/,+5/,+5/,+6/,+5/,+6/,+3/---,++1/,+3/,+3/,+4/,+4/,+2/,+2/,+7/,+7/,+2/,+2/,+3/,+1/,+1/,+6/,+5/,+6/,+1/,+1/,+2/,+2/,7/,+3/,+2/,+3/--,++1/,++1/,++1/,++2/,++2/,++1/,+7/,+6/,+5/,+5/,+6/,+5/,+3/----,++1/,++1/,++1/,++1/,++2/,++2/,++1/,+7/,+6/,+5/,+5/,+6/,+5/,+6/--,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+3/,+2/,+3/,6/,+1/,+4/,+3/,+4/,6/,+1/,+4/,+3/,+4/,+4/,+3/,+4/,+4/,+5/,+5/,+6/,+5/,+6/,+3/---"""
        self.textboxes[0].insert(tk.END, default_score)

        default_score = """[0,120,C大调]-6/,1/,3/,1/,3/,1/,--3/,-6/,1/,3/,1/,3/,1/,3/,1/,-6/,2/,4/,2/,4/,2/,-2/,5/,-7/,-5/,-7/,-5/,-7/,-5/,1/,3/,1/,-3/,6/,1/,-3/,6/,1/,-3/,6/,1/,-2/,-6/,2/,-6/--,-5/--,-1/,-5/,1/-,-3/,-6/,1/,-3/,-6/,1/,-3/,-6/,1/,-2/,-6/,2/,-4/,-6/,1/,-5/,-7/,2/,-1/,-5/,1/,-6/,1/,3/,-6/,2/,4/,-5/,-7/,2/,-5/,1/,3/,-3/,-6/,1/,-4/,-7/,2/,-3/,-4/,-5/,-3/,-6/,1/,-2/,-6/,1/,-2/,-5/,-7/,-3/,-6/,1/,-3/,-6/,1/,-2/,-6/,1/,-2/,-5/,-7/,-3/,-6/,1/,-1/,-5/,1/-,-3/,-6/,1/,-3/,-6/,1/,-3/,-6/,1/,-2/,-6/,2/,-4/,-6/,1/,-5/,-7/,2/,-1/,-5/,1/,-6/,1/,3/,-6/,2/,4/,-5/,-7/,2/,-5/,1/,3/,-3/,-6/,1/,-4/,-7/,2/,-3/,-4/,-5/,-3/,-6/,1/,-2/,-6/,1/,-2/,-5/,-7/,-3/,-6/,1/,-3/,-6/,1/,-2/,-6/,1/,-2/,-5/,-7/,-3/,-6/,1/---"""
        self.textboxes[1].insert(tk.END, default_score)
        # 默认显示第一个音轨文本框
        self.show_textbox(0)

    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 textbox in 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

                # 获取拍子信息(这里假设默认 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))
                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))
            self.play_threads.append(thread)
            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_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.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 get_instrument_number(self, instrument_name):
        # 这里简单映射,实际可能需要更复杂的映射
        if instrument_name == "大钢琴(声学钢琴)":
            return 0
        elif instrument_name == "明亮的钢琴":
            return 1
        elif instrument_name == "电钢琴":
            return 2
        elif instrument_name == "酒吧钢琴":
            return 3

        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 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:
                # 创建一个新的 MIDI 文件
                mid = mido.MidiFile()

                # 处理新增的 16 个音轨
                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)

                        tempo = mido.bpm2tempo(bpm)
                        track.append(mido.MetaMessage('set_tempo', tempo=tempo))

                        numerator, denominator = 4, 4
                        track.append(mido.MetaMessage('time_signature', numerator=numerator, denominator=denominator))

                        track.append(Message('program_change', program=instrument_number, time=0))

                        transpose = self.get_transpose(mode)

                        self._add_notes_to_track(track, score_content, transpose, 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()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EasySoft易软

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值