```python
# pip install pygame mido
# pip install pyinstaller
# pyinstaller --onefile main.py
# pyinstaller --onefile --icon=1.ico main.py
import random
import tkinter as tk
from tkinter import filedialog, messagebox # 导入messagebox模块
import pygame.midi
import time
import threading
import mido # 导入mido库用于创建和保存MIDI文件
# 初始化 pygame.midi
pygame.midi.init()
# 获取 MIDI 输出设备
try:
midi_out = pygame.midi.Output(0)
except pygame.midi.MidiException:
print("无法打开MIDI输出设备,程序将使用默认设置")
midi_out = None
# 乐器名称映射
instrument_names = {
0: "大钢琴(声学钢琴)",
1: "明亮的钢琴",
2: "电钢琴",
}
# 播放控制标志
is_playing = False
is_paused = False
pause_start_time = 0
current_note_index = 0
all_notes = []
loop_playback = False # 新增:循环播放标志
text_boxes = []
play_buttons = []
track_settings = [] # 存储每个音轨的设置(调式、乐器、速度)
threads = [] # 存储所有线程
# 记录当前显示的文本框索引
current_displayed_index = 0
# 音量设置,初始值为127
volume = 127
def play_note(note_number, duration, instrument, velocity):
global is_playing, is_paused, pause_start_time, midi_out
if midi_out:
midi_out.set_instrument(instrument)
midi_out.note_on(note_number, velocity)
start_time = time.time()
while time.time() - start_time < duration / 1000:
if is_paused:
pause_start_time = time.time()
while is_paused:
root.update()
start_time += time.time() - pause_start_time
if not is_playing:
midi_out.note_off(note_number, velocity)
return
midi_out.note_off(note_number, velocity)
def play_next_note(notes, instrument, tune, speed):
global is_playing, loop_playback
while is_playing:
index = 0
while is_playing and index < len(notes):
note = notes[index]
note_number, duration, velocity = process_note(note, speed)
note_number = convert_tune(note_number, tune)
if note_number is not None:
play_note(note_number, duration, instrument, velocity)
index += 1
# 如果不循环播放,则退出循环
if not loop_playback:
break
def play_all_tracks_together():
global is_playing, threads, loop_playback
is_playing = True
is_paused = False
pause_button.config(text="暂停")
threads = []
for i in range(16):
notes_str = text_boxes[i].get("1.0", "end-1c")
if notes_str.strip(): # 检查文本框是否有内容
notes, instrument, speed, tune = parse_notes_and_settings(notes_str)
thread = threading.Thread(target=play_next_note, args=(notes, instrument, tune, speed))
threads.append(thread)
thread.start()
def pause_music():
global is_paused
is_paused = not is_paused
if is_paused:
pause_button.config(text="继续")
else:
pause_button.config(text="暂停")
def stop_music():
global is_playing, is_paused, current_note_index, threads
is_playing = False
is_paused = False
current_note_index = 0
pause_button.config(text="暂停")
# 停止所有线程
for thread in threads:
if thread.is_alive():
thread.join()
root = tk.Tk()
root.title("MIDI简谱播放器 V1.1")
root.geometry("900x800")
# 创建16个文本框和对应的按钮
for i in range(16):
text_box = tk.Text(root, wrap="word", height=5)
text_box.pack(pady=5)
text_box.place(x=10, y=10, width=900, height=50)
text_boxes.append(text_box)
play_button = tk.Button(root, text=f"显示音轨{i + 1}", command=lambda i=i: display_textbox(i))
play_button.pack(pady=5)
play_button.place(x=430, y=5 + i * 35, width=100, height=40)
play_buttons.append(play_button)
# 设置默认显示第一个音轨文本框的内容
if i == 0:
text_box.insert(tk.END,
"G 大调,40,200\n5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------")
# 第3个速度设置,数量越少速度越快。
# 设置默认显示第一个音轨文本框的内容
if i == 10:
text_box.insert(tk.END,
"A 大调,10,400\n5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------")
# 第3个速度设置,数量越少速度越快。
# 设置默认显示第一个音轨文本框的内容
if i == 15:
text_box.insert(tk.END,
"C 大调,0,600\n5-351*--76-1*-5---5-123-212----5-351*--76-1*-5---5-234--7.1----6-1*-1*---7-671*---671*665312----5-351*--76-1*-5---5-234--7.1------")
# 第3个速度设置,数量越少速度越快。
# 创建文本框的右键菜单
def show_textbox_menu(event):
menu = tk.Menu(root, tearoff=0)
menu.add_command(label="剪切", command=lambda: event.widget.event_generate("<<Cut>>"))
menu.add_command(label="复制", command=lambda: event.widget.event_generate("<<Copy>>"))
menu.add_command(label="粘贴", command=lambda: event.widget.event_generate("<<Paste>>"))
menu.add_separator()
menu.add_command(label="全选", command=lambda: event.widget.event_generate("<<SelectAll>>"))
# 新增:全选复制功能(先全选再触发复制)
menu.add_command(
label="全选复制",
command=lambda: (
event.widget.tag_add("sel", "1.0", "end"), # 全选文本
event.widget.event_generate("<<Copy>>") # 触发复制
)
)
# 新增:全选删除功能(直接删除所有文本)
menu.add_separator() # 分隔线优化菜单布局
menu.add_command(
label="全选删除",
command=lambda: event.widget.delete("1.0", "end")
)
menu.post(event.x_root, event.y_root)
for text_box in text_boxes:
text_box.bind("<Button-3>", show_textbox_menu)
def play_music_together(track_index):
global is_playing, loop_playback
is_playing = True
is_paused = False
pause_button.config(text="暂停")
notes_str = text_boxes[track_index].get("1.0", "end-1c")
if notes_str.strip(): # 检查文本框是否有内容
notes, instrument, speed, tune = parse_notes_and_settings(notes_str)
thread = threading.Thread(target=play_next_note, args=(notes, instrument, tune, speed))
thread.start()
thread.join()
is_playing = False
pause_button.config(text="暂停")
def parse_notes_and_settings(notes_str):
lines = notes_str.splitlines()
if len(lines) > 0:
settings = lines[0].split(',')
tune = settings[0].strip()
instrument = int(settings[1])
speed = int(settings[2])
notes_str = '\n'.join(lines[1:])
else:
tune = "C 大调"
instrument = 0
speed = 300
# 分割音符字符串
notes = []
current_note = ""
for char in notes_str:
if char in '1234567':
if current_note: # 遇到新音符,保存前一个音符
notes.append(current_note)
current_note = char # 开始新音符
elif char in '-.*': # 时值、低音、高音标记
current_note += char
# 忽略其他字符
if current_note: # 添加最后一个音符
notes.append(current_note)
return notes, instrument, speed, tune
def process_note(note_str, base_duration):
if not note_str or note_str[0] not in '1234567':
return None, base_duration, 127 # 休止符或无效音符
# 基本音符映射 (C大调中央C=60)
note_mapping = {'1': 60, '2': 62, '3': 64, '4': 65, '5': 67, '6': 69, '7': 71}
# 计算八度偏移(支持任意数量的.和*)
octave_offset = (note_str.count('*') - note_str.count('.')) * 12
# 计算时值(每个-使时值翻倍)
duration = base_duration * (1.5 ** note_str.count('-'))
# 获取最终音符编号(限制在MIDI范围0-127内)
note_number = max(0, min(127, note_mapping[note_str[0]] + octave_offset))
return note_number, duration, 127 # 固定力度127
def convert_tune(note_number, tune):
if note_number is None:
return None
if tune == "C 大调":
return note_number
elif tune == "D 大调":
return note_number + 2
elif tune == "E 大调":
return note_number + 4
elif tune == "F 大调":
return note_number + 5
elif tune == "G 大调":
return note_number + 7
elif tune == "A 大调":
return note_number + 9
elif tune == "B 大调":
return note_number + 11
return note_number
def export_midi():
try:
file_path = filedialog.asksaveasfilename(defaultextension=".mid", filetypes=[("MIDI Files", "*.mid")])
if not file_path:
return
mid = mido.MidiFile(ticks_per_beat=480)
all_tracks = []
max_track_length = 0
for i in range(16):
notes_str = text_boxes[i].get("1.0", "end-1c")
if not notes_str.strip():
continue
notes, instrument, speed, tune = parse_notes_and_settings(notes_str)
if not notes:
continue
track_events = []
current_time = 0
# 为每个音轨添加Program Change消息,指定乐器
program_msg = mido.Message('program_change', program=instrument, channel=i, time=0)
track_events.append((0, program_msg))
for note in notes:
note_number, duration, velocity = process_note(note, speed)
note_number = convert_tune(note_number, tune)
bpm = 120
ms_per_beat = 60000 / bpm
ms_per_tick = ms_per_beat / mid.ticks_per_beat
tick_duration = int(duration / ms_per_tick)
if note_number is not None:
track_events.append(
(current_time, mido.Message('note_on', note=note_number, velocity=velocity, channel=i)))
track_events.append((current_time + tick_duration,
mido.Message('note_off', note=note_number, velocity=velocity, channel=i)))
current_time += tick_duration
if track_events:
all_tracks.append((instrument, tune, track_events))
max_track_length = max(max_track_length, current_time)
if not all_tracks:
messagebox.showinfo("导出提示", "没有可导出的音轨")
return
for i, (instrument, tune, track_events) in enumerate(all_tracks):
track = mido.MidiTrack()
mid.tracks.append(track)
track_name = f'Track {i + 1} - {tune}'
name_bytes = track_name.encode('utf-8')
track.append(mido.MetaMessage('track_name', name=name_bytes.decode('latin-1', 'replace')))
track_events.sort(key=lambda x: x[0])
prev_time = 0
for time_ticks, event in track_events:
delta = time_ticks - prev_time
track.append(event.copy(time=delta))
prev_time = time_ticks
mid.save(file_path)
messagebox.showinfo("导出成功", f"文件已成功导出到 {file_path}")
except Exception as e:
messagebox.showerror("导出错误", f"导出过程中出现错误: {str(e)}")
def display_textbox(index):
global current_displayed_index
for i, text_box in enumerate(text_boxes):
if i == index:
text_box.place(x=10, y=10, width=400, height=400)
else:
text_box.place_forget()
current_displayed_index = index
# 定义音符列表
z = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "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():
random_notes = []
for _ in range(110):
random_note = random.choice(z) + random.choice(a)
random_notes.append(random_note)
# 清空文本框3并插入随机生成的音符
text_boxes[2].delete("1.0", tk.END) # 清空文本框3
text_boxes[2].insert(tk.END, "G 大调,0,500\n" + "".join(random_notes)) # 将随机音符插入文本框3
# 创建生成随机音符的按钮
generate_button = tk.Button(root, text="随机谱曲音轨3", command=generate_random_notes)
generate_button.pack(pady=20)
generate_button.place(x=700, y=80, width=100, height=30)
# 定义音符列表
z1 = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "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():
random_notes = []
for _ in range(110):
random_note = random.choice(z1) + random.choice(a1)
random_notes.append(random_note)
# 清空文本框4并插入随机生成的音符
text_boxes[3].delete("1.0", tk.END) # 清空文本框4
text_boxes[3].insert(tk.END, "G 大调,0,500\n" + "".join(random_notes)) # 将随机音符插入文本框3
# 创建生成随机音符的按钮
generate_button = tk.Button(root, text="随机谱曲音轨4", command=generate_random_notes1)
generate_button.pack(pady=20)
generate_button.place(x=700, y=110, width=100, height=30)
# 定义音符列表
z2 = ["1..", "2..", "3..", "4..", "5..", "6..", "7..", "1.", "2.", "3.", "4.", "5.", "6.", "7.", "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():
random_notes = []
for _ in range(110):
random_note = random.choice(z2) + random.choice(a2)
random_notes.append(random_note)
# 清空文本框5并插入随机生成的音符
text_boxes[4].delete("1.0", tk.END) # 清空文本框5
text_boxes[4].insert(tk.END, "G 大调,0,500\n" + "".join(random_notes)) # 将随机音符插入文本框3
# 创建生成随机音符的按钮
generate_button = tk.Button(root, text="随机谱曲音轨5", command=generate_random_notes2)
generate_button.pack(pady=20)
generate_button.place(x=700, y=140, width=100, height=30)
def clear_all_textboxes():
for text_box in text_boxes:
text_box.delete("1.0", tk.END)
# 创建清空所有文本框的按钮
clear_button = tk.Button(root, text="清空所有", command=clear_all_textboxes)
clear_button.pack(pady=20)
clear_button.place(x=550, y=210, width=100, height=50)
# 创建暂停按钮
pause_button = tk.Button(root, text="暂停", command=pause_music)
pause_button.pack(pady=20)
pause_button.place(x=550, y=60, width=100, height=50)
# 创建停止按钮
stop_button = tk.Button(root, text="停止", command=stop_music)
stop_button.pack(pady=20)
stop_button.place(x=550, y=110, width=100, height=50)
# 创建导出按钮
export_button = tk.Button(root, text="导出", command=export_midi)
export_button.pack(pady=20)
export_button.place(x=550, y=160, width=100, height=50)
# 创建同时播放所有有内容音轨的按钮
play_all_button = tk.Button(root, text="播放全部音轨", command=play_all_tracks_together)
play_all_button.pack(pady=20)
play_all_button.place(x=550, y=10, width=100, height=50)
# 新增:循环播放复选框
def toggle_loop():
global loop_playback
loop_playback = loop_var.get()
loop_var = tk.BooleanVar()
loop_checkbox = tk.Checkbutton(root, text="循环播放", variable=loop_var, command=toggle_loop)
loop_checkbox.pack(pady=5)
loop_checkbox.place(x=700, y=10, width=100, height=30)
# 初始显示第一个文本框
display_textbox(0)
def on_closing():
global midi_out
if midi_out:
midi_out.close()
pygame.midi.quit()
root.destroy()
root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()
```
源代码下载地址:[https://download.csdn.net/download/qq_32257509/91715331](https://download.csdn.net/download/qq_32257509/91715331)
软件项目下载地址:[https://download.csdn.net/download/qq_32257509/91715332](https://download.csdn.net/download/qq_32257509/91715332)
开源项目通过网盘分享的文件:MIDI简谱播放器V1.1.zip
链接: https://pan.baidu.com/s/15g6wZTeuEZtX3Kbt57uK8g?pwd=y9x7 提取码: y9x7
开源项目通过网盘分享的文件:MIDI简谱播放器V1.1软件.zip
链接: https://pan.baidu.com/s/1a-CuhWMThvvOe12ySkxRcw?pwd=hfxh 提取码: hfxh
开源项目通过网盘分享的文件:MIDI简谱播放器 V1.1.exe
链接: https://pan.baidu.com/s/1YB6sRbVBrm3WoDMGourpRA?pwd=urap 提取码: urap