import os
import mido
from mido import Message
import time
import tkinter as tk
import threading
from tkinter import filedialog
import re
import random
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("简易简谱编辑器 V1.4")
self.root.geometry("900x800")
self.title_label = tk.Label(root, text="简易简谱编辑器1.4", height=2)
self.title_label.pack()
self.clear_button = tk.Button(root, text="清空所有", command=self.clear_all_textboxes)
self.clear_button.pack(pady=20)
self.clear_button.place(x=750, y=300, width=100, height=50)
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)
self.volume_scale = tk.Scale(root, from_=0, to=127, orient=tk.HORIZONTAL, label="音量", length=200)
self.volume_scale.set(64)
self.volume_scale.place(x=680, y=5)
self.volume_scale.bind("<B1-Motion>", self.set_volume)
self.generate_button = tk.Button(root, text="随机谱曲音轨3", command=self.generate_random_notes)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=110, width=100, height=30)
self.generate_button = tk.Button(root, text="随机谱曲音轨4", command=self.generate_random_notes1)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=140, width=100, height=30)
self.generate_button = tk.Button(root, text="随机谱曲音轨5", command=self.generate_random_notes2)
self.generate_button.pack(pady=20)
self.generate_button.place(x=690, y=170, width=100, height=30)
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
self.textbox_y_start = -250
self.textbox_width = 500
self.textbox_height = 300
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)
z = ["-1/", "-2/", "-3/", "-4/", "-5/", "-6/", "-7/", "1/", "2/", "3/", "4/", "5/", "6/", "7/", "+1/", "+2/", "+3/",
"+4/", "+5/",
"+6/", "+7/"]
a = [",", "-,", "--,"]
def generate_random_notes(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z) + random.choice(self.a)
random_notes.append(random_note)
self.textboxes[2].delete("1.0", tk.END)
self.textboxes[2].insert(tk.END, "[0,120,G大调]----" + "".join(random_notes))
z1 = ["-1/", "-2/", "-3/", "-4/", "-5/", "-6/", "-7/", "1/", "2/", "3/", "4/", "5/", "6/", "7/", "+1/", "+2/",
"+3/",
"+4/", "+5/",
"+6/", "+7/"]
a1 = [",", "-,", "--,"]
def generate_random_notes1(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z1) + random.choice(self.a1)
random_notes.append(random_note)
self.textboxes[3].delete("1.0", tk.END)
self.textboxes[3].insert(tk.END, "[10,120,G大调]----" + "".join(random_notes))
z2 = ["-1/", "-2/", "-3/", "-4/", "-5/", "-6/", "-7/", "1/", "2/", "3/", "4/", "5/", "6/", "7/", "+1/", "+2/",
"+3/",
"+4/", "+5/",
"+6/", "+7/"]
a2 = [",", "-,", "--,"]
def generate_random_notes2(self):
random_notes = []
for _ in range(110):
random_note = random.choice(self.z2) + random.choice(self.a2)
random_notes.append(random_note)
self.textboxes[4].delete("1.0", tk.END)
self.textboxes[4].insert(tk.END, "[0,120,C大调]----" + "".join(random_notes))
def clear_all_textboxes(self):
for text_box in self.textboxes:
text_box.delete("1.0", tk.END)
def set_volume(self, event):
volume = self.volume_scale.get()
msg = Message('control_change', control=7, value=int(volume), time=0)
self.output_port.send(msg)
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)
quarter_note_duration = 60 / bpm
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)
quarter_note_duration = 60 / bpm
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:
mid = mido.MidiFile()
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)
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()