from music21 import stream, note, meter, clef, environment
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
# Основной класс приложения
class MusicNotationApp:
def __init__(self, root):
self.root = root
self.root.title("Нотный редактор")
self.root.geometry("800x600")
self.root.configure(bg="#f0f0f0")
# Инициализация партитуры
self.score = stream.Score()
self.part = stream.Part()
self.part.insert(0, clef.TrebleClef())
self.part.insert(0, meter.TimeSignature('4/4'))
self.score.append(self.part)
# Переменные интерфейса
self.note_var = tk.StringVar(value="C4")
self.duration_var = tk.StringVar(value="quarter")
self.position_var = tk.StringVar(value="0.0")
self.create_widgets()
self.setup_environment()
def setup_environment(self):
"""Настройка путей для MuseScore"""
try:
# Попробуем автоматически найти MuseScore
if os.name == 'nt': # Windows
paths = [
"C:/Program Files/MuseScore 4/bin/MuseScore4.exe",
"C:/Program Files/MuseScore 3/bin/MuseScore3.exe"
]
else: # Mac/Linux
paths = [
"/Applications/MuseScore 4.app/Contents/MacOS/mscore",
"/usr/bin/musescore",
"/usr/local/bin/musescore"
]
for path in paths:
if os.path.exists(path):
environment.set('musescoreDirectPNGPath', path)
environment.set('musicxmlPath', path)
break
except Exception as e:
messagebox.showerror("Ошибка", f"MuseScore не найден: {str(e)}")
def create_widgets(self):
"""Создание элементов интерфейса"""
# Стили
style = ttk.Style()
style.configure("TFrame", background="#f0f0f0")
style.configure("TButton", padding=6, font=("Arial", 10))
style.configure("TLabel", background="#f0f0f0", font=("Arial", 10))
# Основные фреймы
control_frame = ttk.Frame(self.root, padding=10)
control_frame.pack(fill=tk.X, padx=10, pady=5)
display_frame = ttk.Frame(self.root)
display_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
# Поля ввода
ttk.Label(control_frame, text="Высота ноты (C4, D#3, etc):").grid(row=0, column=0, sticky=tk.W)
note_entry = ttk.Entry(control_frame, textvariable=self.note_var, width=10)
note_entry.grid(row=0, column=1, padx=5)
ttk.Label(control_frame, text="Длительность:").grid(row=0, column=2, sticky=tk.W, padx=(10,0))
durations = ["whole", "half", "quarter", "eighth", "16th"]
duration_combo = ttk.Combobox(control_frame, textvariable=self.duration_var, values=durations, width=8)
duration_combo.grid(row=0, column=3)
ttk.Label(control_frame, text="Позиция:").grid(row=0, column=4, sticky=tk.W, padx=(10,0))
ttk.Entry(control_frame, textvariable=self.position_var, width=8).grid(row=0, column=5)
# Кнопки управления
buttons = [
("Добавить ноту", self.add_note),
("Добавить паузу", self.add_rest),
("Удалить последнюю", self.delete_last),
("Показать партитуру", self.show_score),
("Воспроизвести", self.play_score),
("Сохранить", self.save_score),
("Очистить", self.clear_score)
]
for i, (text, command) in enumerate(buttons):
ttk.Button(control_frame, text=text, command=command).grid(row=1, column=i, padx=5, pady=10)
# Область отображения нот
self.text_display = tk.Text(display_frame, wrap=tk.WORD, font=("Courier New", 12))
self.text_display.pack(fill=tk.BOTH, expand=True)
self.text_display.insert(tk.END, "Партитура пуста. Добавьте ноты.")
self.text_display.config(state=tk.DISABLED)
# Статус бар
self.status_var = tk.StringVar(value="Готово")
status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def add_note(self):
"""Добавление ноты в партитуру"""
try:
pitch = self.note_var.get()
duration_type = self.duration_var.get()
position = float(self.position_var.get())
n = note.Note(pitch)
n.duration.type = duration_type
self.part.insert(position, n)
self.update_display(f"Добавлена нота: {pitch} ({duration_type}) на позиции {position}")
self.status_var.set(f"Добавлена нота: {pitch}")
except Exception as e:
messagebox.showerror("Ошибка", f"Невозможно добавить ноту: {str(e)}")
def add_rest(self):
"""Добавление паузы в партитуру"""
try:
duration_type = self.duration_var.get()
position = float(self.position_var.get())
r = note.Rest()
r.duration.type = duration_type
self.part.insert(position, r)
self.update_display(f"Добавлена пауза: ({duration_type}) на позиции {position}")
self.status_var.set(f"Добавлена пауза")
except Exception as e:
messagebox.showerror("Ошибка", f"Невозможно добавить паузу: {str(e)}")
def delete_last(self):
"""Удаление последнего элемента"""
if len(self.part.notesAndRests) > 0:
last_element = self.part.notesAndRests[-1]
self.part.remove(last_element)
self.update_display("Удалён последний элемент")
self.status_var.set("Удалён последний элемент")
else:
messagebox.showinfo("Информация", "Партитура пуста")
def show_score(self):
"""Отображение нотного стана"""
try:
self.score.show('musicxml.png')
self.status_var.set("Партитура отображена")
except Exception as e:
messagebox.showerror("Ошибка", f"Невозможно отобразить партитуру: {str(e)}\nУстановите MuseScore")
def play_score(self):
"""Воспроизведение MIDI"""
try:
self.score.show('midi')
self.status_var.set("Воспроизведение...")
except Exception as e:
messagebox.showerror("Ошибка", f"Невозможно воспроизвести: {str(e)}")
def save_score(self):
"""Сохранение партитуры в файл"""
try:
file_path = filedialog.asksaveasfilename(
defaultextension=".musicxml",
filetypes=[
("MusicXML", "*.musicxml"),
("MIDI", "*.mid"),
("PDF", "*.pdf")
]
)
if file_path:
if file_path.endswith(".pdf"):
self.score.write('musicxml.pdf', fp=file_path)
elif file_path.endswith(".mid"):
self.score.write('midi', fp=file_path)
else:
self.score.write('musicxml', fp=file_path)
self.status_var.set(f"Файл сохранён: {file_path}")
except Exception as e:
messagebox.showerror("Ошибка", f"Ошибка сохранения: {str(e)}")
def clear_score(self):
"""Очистка партитуры"""
if messagebox.askyesno("Подтверждение", "Очистить всю партитуру?"):
self.part.clear()
self.update_display("Партитура очищена")
self.status_var.set("Партитура очищена")
def update_display(self, action_message):
"""Обновление текстового отображения партитуры"""
self.text_display.config(state=tk.NORMAL)
self.text_display.delete(1.0, tk.END)
if len(self.part.notesAndRests) == 0:
self.text_display.insert(tk.END, "Партитура пуста. Добавьте ноты.")
else:
self.text_display.insert(tk.END, "Текущая партитура:\n\n")
for i, element in enumerate(self.part.notesAndRests):
if isinstance(element, note.Note):
info = f"{i+1}. Нота: {element.pitch} | Длительность: {element.duration.type} | Позиция: {element.offset}"
else:
info = f"{i+1}. Пауза | Длительность: {element.duration.type} | Позиция: {element.offset}"
self.text_display.insert(tk.END, info + "\n")
self.text_display.insert(tk.END, f"\n{action_message}")
self.text_display.config(state=tk.DISABLED)
if __name__ == "__main__":
root = tk.Tk()
app = MusicNotationApp(root)
root.mainloop()