欢迎光临
我们一直在努力

python语言学习机-音乐谱曲+绘画谱曲双模式新代码

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageDraw, ImageTk
import threading
import time
import math
import os
import pygame
import pygame.midi

class MusicAndPaintBoard:
def __init__(self, root):
self.root = root
self.root.title("学习机 – 音乐谱曲 + 绘画谱曲 双模式")
self.root.geometry("1500×900")

# 通用参数
self.current_mode = "music"
self.canvas_width = 800
self.canvas_height = 500
self.play_speed = 200
self.is_playing = False
self.current_frame = 0
self.frames = []
self.commands = []
self.enable_sound = True

# 初始化 MIDI
self.init_midi()

# MIDI 音符编号 (C4=60)
self.midi_notes = {
'1': 60, # C4 – do
'2': 62, # D4 – re
'3': 64, # E4 – mi
'4': 65, # F4 – fa
'5': 67, # G4 – sol
'6': 69, # A4 – la
'7': 71, # B4 – si
}

# 颜色映射
self.note_colors = {
'1': (255, 80, 80), '2': (255, 165, 80), '3': (255, 255, 80),
'4': (80, 255, 80), '5': (80, 150, 255), '6': (128, 80, 255),
'7': (255, 128, 255),
}

# 绘画模式参数
self.pen_x = self.canvas_width // 2
self.pen_y = self.canvas_height // 2
self.pen_angle = 0
self.pen_down = True
self.pen_color = (0, 0, 0)
self.pen_size = 3
self.step_distance = 25

self.setup_ui()
self.init_background()
self.load_demo()

def init_midi(self):
"""初始化 MIDI"""
try:
pygame.init()
pygame.midi.init()
# 打开默认输出设备
self.midi_output = pygame.midi.Output(0)
self.midi_output.set_instrument(0) # 钢琴音色
print("MIDI 初始化成功")
except Exception as e:
print(f"MIDI 初始化失败: {e}")
self.midi_output = None

def play_midi_note(self, note_num, duration, octave_shift=0):
"""播放 MIDI 音符"""
if not self.enable_sound or self.midi_output is None:
return

try:
# 计算 MIDI 音符编号
midi_note = self.midi_notes[str(note_num)]
if octave_shift == 1:
midi_note += 12 # 高八度
elif octave_shift == -1:
midi_note -= 12 # 低八度

# 播放音符 (note_on, 音量100)
self.midi_output.note_on(midi_note, 100)

# 在单独线程中等待后关闭音符
def stop_note():
time.sleep(duration / 1000.0)
self.midi_output.note_off(midi_note, 100)

threading.Thread(target=stop_note, daemon=True).start()

except Exception as e:
print(f"MIDI 播放失败: {e}")

def setup_ui(self):
"""设置界面"""
# 模式切换栏
mode_frame = ttk.Frame(self.root)
mode_frame.pack(fill=tk.X, padx=10, pady=5)

ttk.Label(mode_frame, text="选择模式:", font=("Arial", 12, "bold")).pack(side=tk.LEFT, padx=5)

self.mode_var = tk.StringVar(value="music")
ttk.Radiobutton(mode_frame, text="🎵 音乐谱曲 (MIDI简谱)",
variable=self.mode_var, value="music",
command=self.switch_mode).pack(side=tk.LEFT, padx=10)
ttk.Radiobutton(mode_frame, text="🎨 绘画谱曲 (数字画谱)",
variable=self.mode_var, value="paint",
command=self.switch_mode).pack(side=tk.LEFT, padx=10)

# 声音开关
self.sound_var = tk.BooleanVar(value=True)
ttk.Checkbutton(mode_frame, text="🔊 MIDI 声音", variable=self.sound_var,
command=self.toggle_sound).pack(side=tk.LEFT, padx=20)

# 测试声音按钮
ttk.Button(mode_frame, text="🎵 测试声音", command=self.test_sound).pack(side=tk.LEFT, padx=5)

# 主容器
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)

# 左侧:画布
left_frame = ttk.LabelFrame(main_frame, text="输出区域", padding=10)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

self.canvas = tk.Canvas(left_frame, width=self.canvas_width,
height=self.canvas_height, bg='white',
relief=tk.SUNKEN, borderwidth=2)
self.canvas.pack()

# 状态栏
status_frame = ttk.Frame(left_frame)
status_frame.pack(fill=tk.X, pady=5)
self.status_label = ttk.Label(status_frame, text="就绪 | MIDI 已加载", font=("Arial", 10))
self.status_label.pack(side=tk.LEFT)
self.frame_label = ttk.Label(status_frame, text="")
self.frame_label.pack(side=tk.RIGHT)

# 预览区
preview_frame = ttk.LabelFrame(left_frame, text="帧预览", padding=5)
preview_frame.pack(fill=tk.X, pady=5)
self.preview_canvas = tk.Canvas(preview_frame, height=100, bg='lightgray')
self.preview_canvas.pack(fill=tk.X)

# 右侧:编辑器
right_frame = ttk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(10, 0))

# 乐谱/画谱编辑器
editor_frame = ttk.LabelFrame(right_frame, text="简谱编辑器 (数字序列)", padding=10)
editor_frame.pack(fill=tk.BOTH, expand=True)

# 帮助信息
self.help_text = tk.Text(editor_frame, height=8, font=("Courier", 9),
bg='#f0f0f0', wrap=tk.WORD)
self.help_text.pack(fill=tk.X, pady=5)

# 输入区
input_frame = ttk.Frame(editor_frame)
input_frame.pack(fill=tk.BOTH, expand=True, pady=5)

ttk.Label(input_frame, text="简谱序列:").pack(anchor=tk.W)

text_frame = ttk.Frame(input_frame)
text_frame.pack(fill=tk.BOTH, expand=True)

self.score_text = tk.Text(text_frame, height=10, font=("Courier", 12))
self.score_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

score_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=self.score_text.yview)
score_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.score_text.config(yscrollcommand=score_scrollbar.set)

# 快速音符按钮
notes_frame = ttk.LabelFrame(editor_frame, text="快速输入", padding=5)
notes_frame.pack(fill=tk.X, pady=5)

self.note_buttons_frame = ttk.Frame(notes_frame)
self.note_buttons_frame.pack()
self.update_quick_buttons()

# 控制按钮
control_frame = ttk.Frame(editor_frame)
control_frame.pack(fill=tk.X, pady=5)

ttk.Button(control_frame, text="▶ 播放", command=self.play).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
ttk.Button(control_frame, text="⏸ 暂停", command=self.pause).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
ttk.Button(control_frame, text="⏹ 停止", command=self.stop).pack(side=tk.LEFT, padx=2, expand=True, fill=tk.X)
ttk.Button(control_frame, text="💾 保存", command=self.save_score).pack(side=tk.LEFT, padx=2, expand=True,
fill=tk.X)
ttk.Button(control_frame, text="📁 加载", command=self.load_score).pack(side=tk.LEFT, padx=2, expand=True,
fill=tk.X)
ttk.Button(control_frame, text="📤 导出", command=self.export_file).pack(side=tk.LEFT, padx=2, expand=True,
fill=tk.X)

# 速度控制
speed_frame = ttk.Frame(editor_frame)
speed_frame.pack(fill=tk.X, pady=5)
ttk.Label(speed_frame, text="播放速度:").pack(side=tk.LEFT)
self.speed_var = tk.IntVar(value=120)
speed_scale = ttk.Scale(speed_frame, from_=100, to=1000, variable=self.speed_var,
orient=tk.HORIZONTAL, length=200, command=self.update_speed)
speed_scale.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X)
self.speed_label = ttk.Label(speed_frame, text="120ms")
self.speed_label.pack(side=tk.LEFT)

# 命令列表
list_frame = ttk.LabelFrame(right_frame, text="解析后的序列", padding=5)
list_frame.pack(fill=tk.BOTH, expand=True, pady=5)

self.command_listbox = tk.Listbox(list_frame, height=6, font=("Courier", 10))
self.command_listbox.pack(fill=tk.BOTH, expand=True)

list_scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.command_listbox.yview)
list_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.command_listbox.config(yscrollcommand=list_scrollbar.set)

self.update_help_text()

def test_sound(self):
"""测试 MIDI 声音"""
if self.midi_output:
self.play_midi_note(1, 500, 0)
self.status_label.config(text="测试声音: 播放 do 音")

def toggle_sound(self):
"""切换声音"""
self.enable_sound = self.sound_var.get()

def update_quick_buttons(self):
"""更新快速按钮"""

for widget in self.note_buttons_frame.winfo_children():
widget.destroy()

if self.mode_var.get() == "music":
music_buttons = [
('1 do', '1'), ('2 re', '2'), ('3 mi', '3'), ('4 fa', '4'),
('5 sol', '5'), ('6 la', '6'), ('7 si', '7'),
('- 延长', '-'), ('. 低音', '.'), ("' 高音", "'"),
('| 小节', '|'), ('_ 八分', '_'), ('🎵 示例曲', 'demo_song'), ('清空', 'clear')
]
for i, (text, note) in enumerate(music_buttons):
if note == 'clear':
btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
command=self.clear_score)
else:
btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
command=lambda n=note: self.insert_note(n))
btn.grid(row=i // 7, column=i % 7, padx=2, pady=2)
else:
paint_buttons = [
('1前进', '1'), ('2后退', '2'), ('3左转', '3'), ('4右转', '4'),
('5抬笔', '5'), ('6落笔', '6'), ('7颜色', '7'),
('8画圆', '8'), ('9画方', '9'), ('0清屏', '0'), ('.画点', '.'),
('红', '7 1'), ('绿', '7 2'), ('蓝', '7 3'), ('清空', 'clear'),
('左转45', '3 45'), ('右转45', '4 45')
]
for i, (text, note) in enumerate(paint_buttons):
if note == 'clear':
btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
command=self.clear_score)
else:
btn = ttk.Button(self.note_buttons_frame, text=text, width=6,
command=lambda n=note: self.insert_note(n))
btn.grid(row=i // 9, column=i % 9, padx=2, pady=2)

def update_help_text(self):
"""更新帮助文本"""
self.help_text.delete("1.0", tk.END)

if self.mode_var.get() == "music":
help_text = """
╔══════════════════════════════════════════════════════════════════════════════╗
║ 🎵 音乐简谱编辑器 (MIDI 专业音色) ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ 音符: 1=do 2=re 3=mi 4=fa 5=sol 6=la 7=si ║
║ 时值: – 延长一倍 _ 八分音符 数字后无符号=四分音符 ║
║ 音高: .低音 '高音 (如 1. 低音do, 1' 高音do) ║
║ 使用 MIDI 钢琴音色播放,声音更专业! ║
║ 示例: 1 2 3 4 5 6 7 | 1- 2- 3- 4- | 1. 2. 3. 4. | 1' 2' 3' 4' ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
else:
help_text = """
╔══════════════════════════════════════════════════════════════════════════════╗
║ 🎨 绘画简谱编辑器 (支持任意角度) ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ 1=前进 2=后退 3=左转角度 4=右转角度 5=抬笔 6=落笔 ║
║ 7=颜色 8=画圆 9=画方 0=清屏 .=画点 ║
║ ║
║ 角度用法: ║
║ 3 90 – 左转90度 ║
║ 3 45 – 左转45度 ║
║ 4 30 – 右转30度 ║
║ ║
║ 示例1(正方形): 1 1 1 1 3 90 1 1 1 1 3 90 1 1 1 1 3 90 1 1 1 1 ║
║ 示例2(正六边形): 1 1 1 1 3 60 增加6次 ║
║ 示例3:1 3 30 增加11次 ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""
self.help_text.insert(tk.END, help_text)

def switch_mode(self):
"""切换模式"""
self.update_help_text()
self.update_quick_buttons()
self.init_background()
self.load_demo()
self.stop()

def init_background(self):
"""初始化背景"""
self.background = Image.new('RGB', (self.canvas_width, self.canvas_height), (255, 255, 255))
draw = ImageDraw.Draw(self.background)

if self.mode_var.get() == "music":
staff_y = [150, 180, 210, 240, 270]
for y in staff_y:
draw.line((50, y, self.canvas_width – 50, y), fill=(0, 0, 0), width=1)
draw.text((60, 155), "𝄞", fill=(100, 100, 100))
for x in range(100, self.canvas_width – 50, 100):
draw.line((x, 140, x, 280), fill=(0, 0, 0), width=1)
else:
step = 40
for x in range(0, self.canvas_width, step):
draw.line((x, 0, x, self.canvas_height), fill=(230, 230, 230), width=1)
for y in range(0, self.canvas_height, step):
draw.line((0, y, self.canvas_width, y), fill=(230, 230, 230), width=1)
draw.ellipse((self.canvas_width // 2 – 5, self.canvas_height // 2 – 5,
self.canvas_width // 2 + 5, self.canvas_height // 2 + 5),
fill=(200, 200, 200))
draw.line((0, self.canvas_height // 2, self.canvas_width, self.canvas_height // 2),
fill=(180, 180, 180), width=1)
draw.line((self.canvas_width // 2, 0, self.canvas_width // 2, self.canvas_height),
fill=(180, 180, 180), width=1)

self.image = self.background.copy()
self.update_display()

def load_demo(self):
"""加载演示内容"""
if self.mode_var.get() == "music":
demo = "553-5'5'3'-35513-3-1332126.-6.216.116.-"
else:
demo = "553-5'5'3'-35513-3-1332126.-6.216.116.-"
self.score_text.delete("1.0", tk.END)
self.score_text.insert(tk.END, demo)

def insert_note(self, note):
"""插入音符/指令"""
current = self.score_text.get("1.0", tk.END).strip()
if note == 'clear':
self.clear_score()
elif note == 'demo_song':
self.load_demo_song()
else:
new_text = current + " " + note if current else note
self.score_text.delete("1.0", tk.END)
self.score_text.insert(tk.END, new_text)

def load_demo_song(self):
"""加载示例歌曲(送别变奏曲)"""
demo = "5-351'–76-1'-5—5-123-212—-5-351'–76-1'-5—5-234–7.1—-"
self.score_text.delete("1.0", tk.END)
self.score_text.insert(tk.END, demo)
self.status_label.config(text="已加载示例曲谱,点击播放")

def clear_score(self):
"""清空输入"""
self.score_text.delete("1.0", tk.END)

def parse_music_score(self, text):
"""解析音乐简谱(支持无空格连续格式)"""
notes = []

# 先移除所有空格和换行
text = text.strip().replace(' ', '').replace('\\n', '').replace('\\r', '')

i = 0
length = len(text)

while i < length:
char = text[i]

if char == '|':
notes.append(('bar', 0))
i += 1
continue

# 解析音符 (1-7)
if char in self.midi_notes:
note_num = int(char)
duration = 400 # 默认四分音符
octave_shift = 0
i += 1

# 解析后续修饰符
while i < length:
next_char = text[i]
if next_char == '-':
duration = 800 # 延长一倍
i += 1
elif next_char == '_':
duration = 200 # 八分音符
i += 1
elif next_char == '=':
duration = 100 # 十六分音符
i += 1
elif next_char == '.':
octave_shift = -1 # 低音
i += 1
elif next_char == "'":
octave_shift = 1 # 高音
i += 1
else:
break

notes.append(('note', note_num, octave_shift, duration))

# 跳过其他字符(如空格、换行等已经在开头处理了)
else:
i += 1

return notes

def parse_paint_score(self, text):
"""解析绘画简谱"""
commands = []
tokens = text.split()

i = 0
while i < len(tokens):
token = tokens[i]

if token == '1':
commands.append(('forward', self.step_distance))
elif token == '2':
commands.append(('backward', self.step_distance))
elif token == '3':
angle = 90
if i + 1 < len(tokens):
try:
angle = int(tokens[i + 1])
i += 1
except:
pass
commands.append(('left', angle))
elif token == '4':
angle = 90
if i + 1 < len(tokens):
try:
angle = int(tokens[i + 1])
i += 1
except:
pass
commands.append(('right', angle))
elif token == '5':
commands.append(('pen_up',))
elif token == '6':
commands.append(('pen_down',))
elif token == '7':
if i + 1 < len(tokens):
color_num = tokens[i + 1]
color_map = {'1': (255, 0, 0), '2': (0, 255, 0), '3': (0, 0, 255),
'4': (255, 255, 0), '5': (255, 0, 255), '6': (0, 255, 255)}
if color_num in color_map:
commands.append(('color', color_map[color_num]))
i += 1
elif token == '8':
if i + 1 < len(tokens):
try:
radius = int(tokens[i + 1])
commands.append(('circle', radius))
i += 1
except:
commands.append(('circle', 30))
elif token == '9':
if i + 1 < len(tokens):
try:
size = int(tokens[i + 1])
commands.append(('rect', size))
i += 1
except:
commands.append(('rect', 40))
elif token == '0':
commands.append(('clear',))
elif token == '.':
commands.append(('dot',))

i += 1

return commands

def play(self):
"""播放"""
text = self.score_text.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "请输入简谱内容!")
return

if self.mode_var.get() == "music":
self.play_music(text)
else:
self.play_paint(text)

def play_music(self, text):
"""播放音乐模式(支持无空格连续简谱)"""
notes = self.parse_music_score(text)

if not notes:
messagebox.showwarning("警告", "无法解析乐谱!")
return

self.commands = notes
self.frames = []
self.note_timing = []
current_img = self.background.copy()
draw = ImageDraw.Draw(current_img)

x_pos = 100
frame_index = 0

for note_info in notes:
if note_info[0] == 'bar':
draw.line((x_pos, 140, x_pos, 280), fill=(0, 0, 0), width=2)
x_pos += 30
for _ in range(2):
self.frames.append(current_img.copy())
frame_index += 1
continue

_, note_num, octave, duration = note_info

# 计算Y位置
base_y = 210
y_pos = base_y – (note_num – 4) * 15 – octave * 20
y_pos = max(50, min(self.canvas_height – 50, y_pos))

# 画音符
color = self.note_colors[str(note_num)]
draw.ellipse((x_pos – 10, y_pos – 10, x_pos + 10, y_pos + 10), fill=color, outline=(0, 0, 0), width=1)
draw.line((x_pos + 10, y_pos, x_pos + 10, y_pos – 40), fill=(0, 0, 0), width=2)

# 添加音符文字
octave_mark = ""
if octave == 1:
octave_mark = "'"
elif octave == -1:
octave_mark = "."
draw.text((x_pos – 8, y_pos – 25), f"{note_num}{octave_mark}", fill=(0, 0, 0))

# 时值标记(延长线)
if duration >= 800:
draw.line((x_pos + 15, y_pos – 20, x_pos + 45, y_pos – 20), fill=(0, 0, 0), width=2)
elif duration <= 200:
draw.line((x_pos + 15, y_pos – 20, x_pos + 25, y_pos – 20), fill=(0, 0, 0), width=1)

# 关键修改:根据声音时长计算帧数,让动画和声音同步
# 帧数 = 声音时长(ms) / 每帧时长(ms)
frame_count = max(2, int(duration / self.play_speed))

start_frame = frame_index
for _ in range(frame_count):
self.frames.append(current_img.copy())
frame_index += 1

# 存储音符信息
self.note_timing.append({
'start_frame': start_frame,
'duration': duration, # 使用原始时长
'note_num': note_num,
'octave': octave
})

# 移动位置,根据时值调整间距
if duration >= 800:
x_pos += 55
elif duration <= 200:
x_pos += 35
else:
x_pos += 45

if x_pos > self.canvas_width – 60:
x_pos = 60

# 结束帧
for _ in range(5):
self.frames.append(current_img.copy())

# 显示解析结果
cmd_list = []
for n in notes:
if n[0] == 'note':
mark = ""
if n[2] == -1:
mark = "."
elif n[2] == 1:
mark = "'"
dur_str = ""
if n[3] >= 800:
dur_str = "-"
elif n[3] <= 200:
dur_str = "_"
cmd_list.append(f"{n[1]}{mark}{dur_str}")
self.update_command_list(cmd_list)

self.start_playback_with_sound()
self.status_label.config(text=f"音乐模式 – 解析 {len([n for n in notes if n[0] == 'note'])} 个音符")

def start_playback_with_sound(self):
"""开始播放(带声音同步)"""
if not self.frames:
messagebox.showwarning("警告", "没有生成动画!")
return

self.current_frame = 0
self.is_playing = True
self.sound_triggered = set() # 记录已触发的声音
self.update_display()
threading.Thread(target=self._play_with_sound, daemon=True).start()

def _play_with_sound(self):
"""带声音同步的播放线程"""
while self.is_playing and self.current_frame < len(self.frames):
# 检查是否需要触发音符声音
for note_info in self.note_timing:
frame_idx = note_info['start_frame']
if frame_idx not in self.sound_triggered and self.current_frame >= frame_idx:
# 到达该音符的起始帧,播放声音
self.sound_triggered.add(frame_idx)
self.play_midi_note(
note_info['note_num'],
note_info['duration'],
note_info['octave']
)

time.sleep(self.play_speed / 1000.0)
self.current_frame += 1
self.root.after(0, self.update_display)
self.root.after(0, self.update_preview)

def play_paint(self, text):
"""播放绘画模式"""
commands = self.parse_paint_score(text)

if not commands:
messagebox.showwarning("警告", "无法解析绘画指令!")
return

self.commands = commands

self.pen_x = self.canvas_width // 2
self.pen_y = self.canvas_height // 2
self.pen_angle = 0
self.pen_down = True
self.pen_color = (0, 0, 0)

self.frames = []
current_img = self.background.copy()
draw = ImageDraw.Draw(current_img)

for cmd in commands:
if cmd[0] == 'forward':
step = cmd[1]
rad = math.radians(self.pen_angle)
new_x = self.pen_x + step * math.cos(rad)
new_y = self.pen_y + step * math.sin(rad)
if self.pen_down:
draw.line((self.pen_x, self.pen_y, new_x, new_y),
fill=self.pen_color, width=self.pen_size)
self.pen_x, self.pen_y = new_x, new_y
for _ in range(10):
self.frames.append(current_img.copy())

elif cmd[0] == 'backward':
step = cmd[1]
rad = math.radians(self.pen_angle)
new_x = self.pen_x – step * math.cos(rad)
new_y = self.pen_y – step * math.sin(rad)
if self.pen_down:
draw.line((self.pen_x, self.pen_y, new_x, new_y),
fill=self.pen_color, width=self.pen_size)
self.pen_x, self.pen_y = new_x, new_y
for _ in range(10):
self.frames.append(current_img.copy())

elif cmd[0] == 'left':
self.pen_angle -= cmd[1]
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'right':
self.pen_angle += cmd[1]
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'pen_up':
self.pen_down = False
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'pen_down':
self.pen_down = True
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'color':
self.pen_color = cmd[1]
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'circle':
radius = cmd[1]
draw.ellipse((self.pen_x – radius, self.pen_y – radius,
self.pen_x + radius, self.pen_y + radius),
outline=self.pen_color, width=self.pen_size)
for _ in range(15):
self.frames.append(current_img.copy())

elif cmd[0] == 'rect':
size = cmd[1]
draw.rectangle((self.pen_x – size, self.pen_y – size,
self.pen_x + size, self.pen_y + size),
outline=self.pen_color, width=self.pen_size)
for _ in range(15):
self.frames.append(current_img.copy())

elif cmd[0] == 'clear':
current_img = self.background.copy()
draw = ImageDraw.Draw(current_img)
for _ in range(5):
self.frames.append(current_img.copy())

elif cmd[0] == 'dot':
draw.ellipse((self.pen_x – 4, self.pen_y – 4,
self.pen_x + 4, self.pen_y + 4),
fill=self.pen_color)
for _ in range(10):
self.frames.append(current_img.copy())

cmd_names = {
'forward': '前进', 'backward': '后退', 'left': '左转', 'right': '右转',
'pen_up': '抬笔', 'pen_down': '落笔', 'color': '换色', 'circle': '画圆',
'rect': '画方', 'clear': '清屏', 'dot': '画点'
}

display_cmds = []
for c in commands:
if c[0] == 'left':
display_cmds.append(f'左转{c[1]}°')
elif c[0] == 'right':
display_cmds.append(f'右转{c[1]}°')
else:
display_cmds.append(cmd_names.get(c[0], c[0]))
self.update_command_list(display_cmds)

self.start_playback()
self.status_label.config(text=f"绘画模式 – 已生成 {len(self.frames)} 帧")

def update_command_list(self, commands):
"""更新命令列表"""
self.command_listbox.delete(0, tk.END)
for i, cmd in enumerate(commands[:30]):
self.command_listbox.insert(tk.END, f"{i + 1}: {cmd}")

def start_playback(self):
"""开始播放"""
if not self.frames:
messagebox.showwarning("警告", "没有生成动画!")
return

self.current_frame = 0
self.is_playing = True
self.update_display()
threading.Thread(target=self._play, daemon=True).start()

def _play(self):
while self.is_playing and self.current_frame < len(self.frames):
time.sleep(self.play_speed / 1000.0)
self.current_frame += 1
self.root.after(0, self.update_display)
self.root.after(0, self.update_preview)

def update_display(self):
"""更新显示"""
if self.current_frame < len(self.frames):
img = self.frames[self.current_frame]
img_resized = img.resize((self.canvas_width, self.canvas_height), Image.Resampling.LANCZOS)
img_tk = ImageTk.PhotoImage(img_resized)
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=img_tk)
self.canvas.image = img_tk
self.frame_label.config(text=f"帧: {self.current_frame + 1}/{len(self.frames)}")

def update_preview(self):
"""更新预览"""
self.preview_canvas.delete("all")
if not self.frames:
return

preview_height = 80
preview_width = int(preview_height * self.canvas_width / self.canvas_height)
cols = min(10, len(self.frames))

for i in range(cols):
x = i * (preview_width + 5) + 5
frame_idx = int(i * len(self.frames) / cols)
if frame_idx < len(self.frames):
img_small = self.frames[frame_idx].resize((preview_width, preview_height), Image.Resampling.LANCZOS)
img_tk = ImageTk.PhotoImage(img_small)
self.preview_canvas.create_image(x, 10, anchor=tk.NW, image=img_tk)
self.preview_canvas.image = img_tk

def pause(self):
self.is_playing = False
self.status_label.config(text="已暂停")

def stop(self):
self.is_playing = False
self.current_frame = 0
self.update_display()
self.status_label.config(text="已停止")

def update_speed(self, *args):
self.play_speed = self.speed_var.get()
self.speed_label.config(text=f"{self.play_speed}ms")

def save_score(self):
"""保存乐谱/画谱"""
text = self.score_text.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "没有内容可保存!")
return

ext = ".music" if self.mode_var.get() == "music" else ".paint"
filename = filedialog.asksaveasfilename(defaultextension=ext,
filetypes=[("简谱文件", f"*{ext}"), ("文本文件", "*.txt")])
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(text)
messagebox.showinfo("成功", f"已保存到: {filename}")
except Exception as e:
messagebox.showerror("错误", f"保存失败: {str(e)}")

def load_score(self):
"""加载乐谱/画谱"""
ext = ".music" if self.mode_var.get() == "music" else ".paint"
filename = filedialog.askopenfilename(filetypes=[("简谱文件", f"*{ext}"), ("文本文件", "*.txt")])
if filename:
try:
with open(filename, 'r', encoding='utf-8') as f:
text = f.read()
self.score_text.delete("1.0", tk.END)
self.score_text.insert(tk.END, text)
messagebox.showinfo("成功", "已加载文件")
except Exception as e:
messagebox.showerror("错误", f"加载失败: {str(e)}")

def export_file(self):
"""导出文件"""
if self.mode_var.get() == "music":
self.export_text()
else:
self.export_gif()

def export_text(self):
"""导出文本格式"""
text = self.score_text.get("1.0", tk.END).strip()
filename = filedialog.asksaveasfilename(defaultextension=".txt",
filetypes=[("文本文件", "*.txt")])
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(f"=== 音乐简谱 ===\\n")
f.write(f"速度: {self.play_speed}ms/拍\\n")
f.write(f"序列:\\n{text}\\n")
messagebox.showinfo("成功", "已导出文件")
except Exception as e:
messagebox.showerror("错误", f"导出失败: {str(e)}")

def export_gif(self):
"""导出GIF"""
if not self.frames:
messagebox.showwarning("警告", "没有动画可导出!请先播放生成动画")
return

filename = filedialog.asksaveasfilename(defaultextension=".gif",
filetypes=[("GIF动画", "*.gif")])
if filename:
try:
self.frames[0].save(filename, save_all=True,
append_images=self.frames[1:],
duration=self.play_speed, loop=0)
messagebox.showinfo("成功", f"GIF已保存!")
except Exception as e:
messagebox.showerror("错误", f"导出失败: {str(e)}")

def __del__(self):
"""清理 MIDI 资源"""
try:
if hasattr(self, 'midi_output') and self.midi_output:
self.midi_output.close()
pygame.midi.quit()
pygame.quit()
except:
pass

if __name__ == "__main__":
root = tk.Tk()
app = MusicAndPaintBoard(root)
root.mainloop()

赞(0)
未经允许不得转载:171主机测评 » python语言学习机-音乐谱曲+绘画谱曲双模式新代码
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址