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("1500x900")
# 通用参数
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)