# -*- 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()
python语言mido16音轨简谱播放器QZQ-2025-4-24
最新推荐文章于 2025-05-01 23:36:42 发布