|
| 1 | +"""Tkinter desktop interface for Python Music Composer.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import os |
| 6 | +from pathlib import Path |
| 7 | +import subprocess |
| 8 | +import sys |
| 9 | +import tkinter as tk |
| 10 | +from tkinter import filedialog, messagebox, ttk |
| 11 | + |
| 12 | +from .composer import compose_melody |
| 13 | +from .harmony import PROGRESSIONS, build_accompaniment |
| 14 | +from .midi import write_midi |
| 15 | + |
| 16 | +ROOT_NOTES = ("C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B") |
| 17 | +SCALES = {"Maior": "major", "Menor natural": "minor"} |
| 18 | +TIME_SIGNATURES = {"4/4": 4, "3/4": 3, "2/4": 2} |
| 19 | +INSTRUMENTS = { |
| 20 | + "Piano": 0, "Piano elétrico": 4, "Violão": 24, "Guitarra limpa": 27, |
| 21 | + "Baixo acústico": 32, "Violino": 40, "Violoncelo": 42, "Cordas": 48, |
| 22 | + "Trompete": 56, "Sax alto": 65, "Flauta": 73, "Sintetizador": 80, |
| 23 | +} |
| 24 | + |
| 25 | + |
| 26 | +class MusicComposerApp(ttk.Frame): |
| 27 | + def __init__(self, master: tk.Tk) -> None: |
| 28 | + super().__init__(master, padding=18) |
| 29 | + self.master = master |
| 30 | + self.last_output: Path | None = None |
| 31 | + self.master.title("Python Music Composer") |
| 32 | + self.master.minsize(650, 650) |
| 33 | + self.master.columnconfigure(0, weight=1) |
| 34 | + self.master.rowconfigure(0, weight=1) |
| 35 | + self.grid(sticky="nsew") |
| 36 | + self.columnconfigure(1, weight=1) |
| 37 | + self._build_variables() |
| 38 | + self._build_layout() |
| 39 | + |
| 40 | + def _build_variables(self) -> None: |
| 41 | + self.root_note = tk.StringVar(value="C") |
| 42 | + self.scale_name = tk.StringVar(value="Maior") |
| 43 | + self.octave = tk.IntVar(value=4) |
| 44 | + self.bars = tk.IntVar(value=8) |
| 45 | + self.time_signature = tk.StringVar(value="4/4") |
| 46 | + self.tempo = tk.IntVar(value=100) |
| 47 | + self.instrument = tk.StringVar(value="Piano") |
| 48 | + self.progression = tk.StringVar(value="I-V-vi-IV") |
| 49 | + self.harmony_enabled = tk.BooleanVar(value=True) |
| 50 | + self.randomness = tk.IntVar(value=45) |
| 51 | + self.seed = tk.StringVar(value="") |
| 52 | + self.output = tk.StringVar(value="composition.mid") |
| 53 | + self.status = tk.StringVar(value="Pronto para compor.") |
| 54 | + |
| 55 | + def _build_layout(self) -> None: |
| 56 | + ttk.Label(self, text="Python Music Composer", font=("TkDefaultFont", 18, "bold")).grid(row=0, column=0, columnspan=3, sticky="w") |
| 57 | + ttk.Label(self, text="Melodia, harmonia e baixo construídos a partir da teoria musical.").grid(row=1, column=0, columnspan=3, sticky="w", pady=(4, 16)) |
| 58 | + self._combo(2, "Tonalidade", self.root_note, ROOT_NOTES) |
| 59 | + self._combo(3, "Escala", self.scale_name, tuple(SCALES)) |
| 60 | + self._spin(4, "Oitava da melodia", self.octave, 1, 7) |
| 61 | + self._spin(5, "Compassos", self.bars, 1, 64) |
| 62 | + self._combo(6, "Compasso", self.time_signature, tuple(TIME_SIGNATURES)) |
| 63 | + self._spin(7, "BPM", self.tempo, 40, 240) |
| 64 | + self._combo(8, "Instrumento melódico", self.instrument, tuple(INSTRUMENTS)) |
| 65 | + self._combo(9, "Progressão harmônica", self.progression, tuple(PROGRESSIONS)) |
| 66 | + ttk.Checkbutton(self, text="Adicionar acordes e linha de baixo", variable=self.harmony_enabled).grid(row=10, column=0, columnspan=3, sticky="w", pady=6) |
| 67 | + |
| 68 | + ttk.Label(self, text="Aleatoriedade").grid(row=11, column=0, sticky="w", pady=6) |
| 69 | + ttk.Scale(self, from_=0, to=100, variable=self.randomness, command=self._update_randomness).grid(row=11, column=1, sticky="ew", padx=(10, 8)) |
| 70 | + self.randomness_label = ttk.Label(self, text="45%", width=5) |
| 71 | + self.randomness_label.grid(row=11, column=2, sticky="w") |
| 72 | + ttk.Label(self, text="Seed (opcional)").grid(row=12, column=0, sticky="w", pady=6) |
| 73 | + ttk.Entry(self, textvariable=self.seed).grid(row=12, column=1, columnspan=2, sticky="ew", padx=(10, 0)) |
| 74 | + ttk.Label(self, text="Arquivo MIDI").grid(row=13, column=0, sticky="w", pady=6) |
| 75 | + ttk.Entry(self, textvariable=self.output).grid(row=13, column=1, sticky="ew", padx=(10, 8)) |
| 76 | + ttk.Button(self, text="Escolher...", command=self.choose_output).grid(row=13, column=2, sticky="ew") |
| 77 | + |
| 78 | + controls = ttk.Frame(self) |
| 79 | + controls.grid(row=14, column=0, columnspan=3, sticky="ew", pady=(20, 10)) |
| 80 | + controls.columnconfigure((0, 1), weight=1) |
| 81 | + ttk.Button(controls, text="Gerar música", command=self.generate).grid(row=0, column=0, sticky="ew", padx=(0, 6)) |
| 82 | + self.open_button = ttk.Button(controls, text="Abrir MIDI", command=self.open_last_output, state="disabled") |
| 83 | + self.open_button.grid(row=0, column=1, sticky="ew", padx=(6, 0)) |
| 84 | + ttk.Separator(self).grid(row=15, column=0, columnspan=3, sticky="ew", pady=(4, 10)) |
| 85 | + ttk.Label(self, textvariable=self.status).grid(row=16, column=0, columnspan=3, sticky="w") |
| 86 | + |
| 87 | + def _combo(self, row, label, variable, values) -> None: |
| 88 | + ttk.Label(self, text=label).grid(row=row, column=0, sticky="w", pady=6) |
| 89 | + ttk.Combobox(self, textvariable=variable, values=values, state="readonly").grid(row=row, column=1, columnspan=2, sticky="ew", padx=(10, 0)) |
| 90 | + |
| 91 | + def _spin(self, row, label, variable, minimum, maximum) -> None: |
| 92 | + ttk.Label(self, text=label).grid(row=row, column=0, sticky="w", pady=6) |
| 93 | + ttk.Spinbox(self, textvariable=variable, from_=minimum, to=maximum).grid(row=row, column=1, columnspan=2, sticky="ew", padx=(10, 0)) |
| 94 | + |
| 95 | + def _update_randomness(self, _value: str) -> None: |
| 96 | + self.randomness_label.configure(text=f"{self.randomness.get()}%") |
| 97 | + |
| 98 | + def choose_output(self) -> None: |
| 99 | + filename = filedialog.asksaveasfilename(title="Salvar composição MIDI", defaultextension=".mid", filetypes=(("MIDI", "*.mid"), ("Todos", "*.*"))) |
| 100 | + if filename: |
| 101 | + self.output.set(filename) |
| 102 | + |
| 103 | + def _seed_value(self) -> int | None: |
| 104 | + raw = self.seed.get().strip() |
| 105 | + if not raw: |
| 106 | + return None |
| 107 | + try: |
| 108 | + return int(raw) |
| 109 | + except ValueError as exc: |
| 110 | + raise ValueError("A seed deve ser um número inteiro.") from exc |
| 111 | + |
| 112 | + def generate(self) -> None: |
| 113 | + try: |
| 114 | + beats = TIME_SIGNATURES[self.time_signature.get()] |
| 115 | + progression = self.progression.get() |
| 116 | + melody = compose_melody( |
| 117 | + root=self.root_note.get(), scale=SCALES[self.scale_name.get()], octave=self.octave.get(), |
| 118 | + bars=self.bars.get(), beats_per_bar=beats, randomness=self.randomness.get() / 100, |
| 119 | + seed=self._seed_value(), progression=progression, |
| 120 | + ) |
| 121 | + chords, bass = ([], []) |
| 122 | + if self.harmony_enabled.get(): |
| 123 | + chords, bass = build_accompaniment(self.root_note.get(), SCALES[self.scale_name.get()], self.bars.get(), beats, progression) |
| 124 | + output = Path(self.output.get().strip() or "composition.mid") |
| 125 | + if output.suffix.lower() not in {".mid", ".midi"}: |
| 126 | + output = output.with_suffix(".mid") |
| 127 | + self.output.set(str(output)) |
| 128 | + path = write_midi(melody, output, self.tempo.get(), INSTRUMENTS[self.instrument.get()], chords, bass, beats) |
| 129 | + except (OSError, ValueError, KeyError, tk.TclError) as exc: |
| 130 | + messagebox.showerror("Não foi possível gerar a música", str(exc)) |
| 131 | + self.status.set("Erro ao gerar composição.") |
| 132 | + return |
| 133 | + self.last_output = path.resolve() |
| 134 | + self.open_button.configure(state="normal") |
| 135 | + self.status.set(f"Criado: {self.last_output} — {len(melody)} notas, {len(chords)} acordes") |
| 136 | + messagebox.showinfo("Composição concluída", f"MIDI criado:\n{self.last_output}") |
| 137 | + |
| 138 | + def open_last_output(self) -> None: |
| 139 | + if not self.last_output or not self.last_output.exists(): |
| 140 | + return |
| 141 | + try: |
| 142 | + if sys.platform.startswith("win"): |
| 143 | + os.startfile(self.last_output) # type: ignore[attr-defined] |
| 144 | + elif sys.platform == "darwin": |
| 145 | + subprocess.run(["open", str(self.last_output)], check=True) |
| 146 | + else: |
| 147 | + subprocess.run(["xdg-open", str(self.last_output)], check=True) |
| 148 | + except (OSError, subprocess.CalledProcessError) as exc: |
| 149 | + messagebox.showerror("Não foi possível abrir o MIDI", str(exc)) |
| 150 | + |
| 151 | + |
| 152 | +def run_gui() -> None: |
| 153 | + root = tk.Tk() |
| 154 | + MusicComposerApp(root) |
| 155 | + root.mainloop() |
0 commit comments