642 lines
23 KiB
Python
642 lines
23 KiB
Python
"""
|
|
PianoMaster 3000 - Ultimate Bell Melody Recorder
|
|
Created for ESP32 Bell System
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
import keyboard
|
|
import time
|
|
import threading
|
|
from math import gcd
|
|
from functools import reduce
|
|
from collections import defaultdict
|
|
|
|
class PianoMaster3000:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
self.root.title("PianoMaster 3000 🎹")
|
|
self.root.geometry("1400x900")
|
|
self.root.configure(bg='#1a1a1a')
|
|
|
|
# Recording state
|
|
self.is_recording = False
|
|
self.recording_started = False
|
|
self.raw_presses = [] # List of (timestamp, keys_pressed)
|
|
self.start_time = None
|
|
|
|
# Settings
|
|
self.num_keys = 8
|
|
self.tempo_mode = tk.StringVar(value="AUTO")
|
|
self.manual_tempo = tk.IntVar(value=100)
|
|
self.precision_mode = tk.StringVar(value="AUTO")
|
|
self.precision_ms = tk.IntVar(value=10)
|
|
self.precision_percent = tk.IntVar(value=5)
|
|
self.chord_window = tk.IntVar(value=50) # Chord detection window in ms
|
|
|
|
# Metronome
|
|
self.metronome_running = False
|
|
self.metronome_thread = None
|
|
|
|
# Create UI
|
|
self.create_ui()
|
|
|
|
# Keyboard monitoring
|
|
self.key_states = {str(i+1): False for i in range(self.num_keys)}
|
|
self.setup_keyboard_listeners()
|
|
|
|
def create_ui(self):
|
|
# Title
|
|
title_label = tk.Label(
|
|
self.root,
|
|
text="🎹 PianoMaster 3000 🎹",
|
|
font=("Arial", 24, "bold"),
|
|
bg='#1a1a1a',
|
|
fg='#00ff00'
|
|
)
|
|
title_label.pack(pady=10)
|
|
|
|
subtitle = tk.Label(
|
|
self.root,
|
|
text="The Ultimate Bell Melody Recorder",
|
|
font=("Arial", 12),
|
|
bg='#1a1a1a',
|
|
fg='#888888'
|
|
)
|
|
subtitle.pack()
|
|
|
|
# Settings Frame
|
|
settings_frame = tk.Frame(self.root, bg='#2a2a2a', relief=tk.RAISED, borderwidth=2)
|
|
settings_frame.pack(pady=15, padx=20, fill=tk.X)
|
|
|
|
# Tempo Settings
|
|
tempo_frame = tk.LabelFrame(settings_frame, text="Tempo Settings", bg='#2a2a2a', fg='white', font=("Arial", 10, "bold"))
|
|
tempo_frame.grid(row=0, column=0, padx=10, pady=10, sticky='new')
|
|
|
|
tk.Radiobutton(tempo_frame, text="AUTO", variable=self.tempo_mode, value="AUTO",
|
|
bg='#2a2a2a', fg='white', selectcolor='#3a3a3a',
|
|
command=self.update_tempo_mode).pack(anchor='w', padx=5, pady=2)
|
|
|
|
manual_frame = tk.Frame(tempo_frame, bg='#2a2a2a')
|
|
manual_frame.pack(anchor='w', padx=5, pady=2)
|
|
tk.Radiobutton(manual_frame, text="Manual:", variable=self.tempo_mode, value="MANUAL",
|
|
bg='#2a2a2a', fg='white', selectcolor='#3a3a3a',
|
|
command=self.update_tempo_mode).pack(side=tk.LEFT)
|
|
tk.Entry(manual_frame, textvariable=self.manual_tempo, width=8).pack(side=tk.LEFT, padx=5)
|
|
tk.Label(manual_frame, text="ms", bg='#2a2a2a', fg='white').pack(side=tk.LEFT)
|
|
|
|
# Precision Settings
|
|
precision_frame = tk.LabelFrame(settings_frame, text="Precision Settings", bg='#2a2a2a', fg='white', font=("Arial", 10, "bold"))
|
|
precision_frame.grid(row=0, column=1, padx=10, pady=10, sticky='new')
|
|
|
|
tk.Radiobutton(precision_frame, text="AUTO", variable=self.precision_mode, value="AUTO",
|
|
bg='#2a2a2a', fg='white', selectcolor='#3a3a3a').pack(anchor='w', padx=5, pady=2)
|
|
|
|
ms_frame = tk.Frame(precision_frame, bg='#2a2a2a')
|
|
ms_frame.pack(anchor='w', padx=5, pady=2)
|
|
tk.Radiobutton(ms_frame, text="±", variable=self.precision_mode, value="MS",
|
|
bg='#2a2a2a', fg='white', selectcolor='#3a3a3a').pack(side=tk.LEFT)
|
|
tk.Entry(ms_frame, textvariable=self.precision_ms, width=6).pack(side=tk.LEFT, padx=5)
|
|
tk.Label(ms_frame, text="ms", bg='#2a2a2a', fg='white').pack(side=tk.LEFT)
|
|
|
|
percent_frame = tk.Frame(precision_frame, bg='#2a2a2a')
|
|
percent_frame.pack(anchor='w', padx=5, pady=2)
|
|
tk.Radiobutton(percent_frame, text="±", variable=self.precision_mode, value="PERCENT",
|
|
bg='#2a2a2a', fg='white', selectcolor='#3a3a3a').pack(side=tk.LEFT)
|
|
tk.Entry(percent_frame, textvariable=self.precision_percent, width=6).pack(side=tk.LEFT, padx=5)
|
|
tk.Label(percent_frame, text="% of tempo", bg='#2a2a2a', fg='white').pack(side=tk.LEFT)
|
|
|
|
# Chord Detection Settings
|
|
chord_frame = tk.LabelFrame(settings_frame, text="Chord Detection", bg='#2a2a2a', fg='white', font=("Arial", 10, "bold"))
|
|
chord_frame.grid(row=0, column=2, padx=10, pady=10, sticky='new')
|
|
|
|
chord_label_frame = tk.Frame(chord_frame, bg='#2a2a2a')
|
|
chord_label_frame.pack(anchor='w', padx=5, pady=5)
|
|
tk.Label(chord_label_frame, text="Combine within:", bg='#2a2a2a', fg='white').pack(anchor='w')
|
|
|
|
chord_input_frame = tk.Frame(chord_frame, bg='#2a2a2a')
|
|
chord_input_frame.pack(anchor='w', padx=5, pady=2)
|
|
tk.Entry(chord_input_frame, textvariable=self.chord_window, width=6).pack(side=tk.LEFT)
|
|
tk.Label(chord_input_frame, text="ms", bg='#2a2a2a', fg='white').pack(side=tk.LEFT, padx=5)
|
|
|
|
tk.Label(chord_frame, text="(Lower = stricter)",
|
|
bg='#2a2a2a', fg='#888888', font=("Arial", 8)).pack(anchor='w', padx=5, pady=2)
|
|
|
|
settings_frame.columnconfigure(0, weight=1)
|
|
settings_frame.columnconfigure(1, weight=1)
|
|
settings_frame.columnconfigure(2, weight=1)
|
|
|
|
# Piano Keys Display
|
|
keys_frame = tk.Frame(self.root, bg='#1a1a1a')
|
|
keys_frame.pack(pady=20)
|
|
|
|
tk.Label(keys_frame, text="Press keys 1-8 on your keyboard",
|
|
font=("Arial", 10), bg='#1a1a1a', fg='#888888').pack()
|
|
|
|
self.key_buttons = []
|
|
keys_container = tk.Frame(keys_frame, bg='#1a1a1a')
|
|
keys_container.pack(pady=10)
|
|
|
|
for i in range(self.num_keys):
|
|
btn = tk.Label(
|
|
keys_container,
|
|
text=str(i+1),
|
|
width=6,
|
|
height=8,
|
|
bg='white',
|
|
fg='black',
|
|
font=("Arial", 16, "bold"),
|
|
relief=tk.RAISED,
|
|
borderwidth=3
|
|
)
|
|
btn.pack(side=tk.LEFT, padx=3)
|
|
self.key_buttons.append(btn)
|
|
|
|
# Status Display
|
|
status_frame = tk.Frame(self.root, bg='#2a2a2a', relief=tk.SUNKEN, borderwidth=2)
|
|
status_frame.pack(pady=10, padx=20, fill=tk.BOTH, expand=True)
|
|
|
|
self.status_label = tk.Label(
|
|
status_frame,
|
|
text="Ready to record! Press START RECORDING, then play your melody.",
|
|
font=("Arial", 11),
|
|
bg='#2a2a2a',
|
|
fg='#00ff00',
|
|
wraplength=800
|
|
)
|
|
self.status_label.pack(pady=5)
|
|
|
|
self.info_label = tk.Label(
|
|
status_frame,
|
|
text="Steps: 0 | Time: 0.00s",
|
|
font=("Arial", 10),
|
|
bg='#2a2a2a',
|
|
fg='#ffff00'
|
|
)
|
|
self.info_label.pack(pady=5)
|
|
|
|
# Melody preview
|
|
self.melody_text = tk.Text(
|
|
status_frame,
|
|
height=8,
|
|
width=80,
|
|
bg='#1a1a1a',
|
|
fg='#00ff00',
|
|
font=("Courier", 10),
|
|
wrap=tk.WORD
|
|
)
|
|
self.melody_text.pack(pady=10, padx=10)
|
|
self.melody_text.insert('1.0', "Melody will appear here as you play...")
|
|
self.melody_text.config(state=tk.DISABLED)
|
|
|
|
# Control Buttons
|
|
button_frame = tk.Frame(self.root, bg='#1a1a1a')
|
|
button_frame.pack(pady=15)
|
|
|
|
self.record_btn = tk.Button(
|
|
button_frame,
|
|
text="🔴 START RECORDING",
|
|
command=self.start_recording,
|
|
bg='#ff0000',
|
|
fg='white',
|
|
font=("Arial", 12, "bold"),
|
|
width=20,
|
|
height=2
|
|
)
|
|
self.record_btn.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.stop_btn = tk.Button(
|
|
button_frame,
|
|
text="⏹ STOP RECORDING",
|
|
command=self.stop_recording,
|
|
bg='#666666',
|
|
fg='white',
|
|
font=("Arial", 12, "bold"),
|
|
width=20,
|
|
height=2,
|
|
state=tk.DISABLED
|
|
)
|
|
self.stop_btn.pack(side=tk.LEFT, padx=5)
|
|
|
|
self.save_btn = tk.Button(
|
|
button_frame,
|
|
text="💾 SAVE MELODY",
|
|
command=self.save_melody,
|
|
bg='#0066cc',
|
|
fg='white',
|
|
font=("Arial", 12, "bold"),
|
|
width=20,
|
|
height=2,
|
|
state=tk.DISABLED
|
|
)
|
|
self.save_btn.pack(side=tk.LEFT, padx=5)
|
|
|
|
# Metronome indicator
|
|
self.metronome_indicator = tk.Label(
|
|
self.root,
|
|
text="",
|
|
font=("Arial", 20, "bold"),
|
|
bg='#1a1a1a',
|
|
fg='#ff0000'
|
|
)
|
|
self.metronome_indicator.pack(pady=5)
|
|
|
|
def update_tempo_mode(self):
|
|
"""Update UI based on tempo mode selection"""
|
|
if self.tempo_mode.get() == "MANUAL" and not self.is_recording:
|
|
# Could start metronome here if desired
|
|
pass
|
|
|
|
def setup_keyboard_listeners(self):
|
|
"""Set up keyboard event listeners for keys 1-8"""
|
|
for i in range(self.num_keys):
|
|
key = str(i + 1)
|
|
keyboard.on_press_key(key, lambda e, k=key: self.on_key_press(k))
|
|
keyboard.on_release_key(key, lambda e, k=key: self.on_key_release(k))
|
|
|
|
def on_key_press(self, key):
|
|
"""Handle key press event"""
|
|
if not self.is_recording:
|
|
return
|
|
|
|
# Ignore if key is already marked as pressed (prevents duplicates)
|
|
if self.key_states[key]:
|
|
return
|
|
|
|
# Start timing on first key press
|
|
if not self.recording_started:
|
|
self.recording_started = True
|
|
self.start_time = time.time()
|
|
self.update_status("Recording... Play your melody!")
|
|
|
|
# Mark key as pressed
|
|
self.key_states[key] = True
|
|
|
|
# Visual feedback
|
|
key_idx = int(key) - 1
|
|
self.key_buttons[key_idx].config(bg='#ffff00')
|
|
|
|
# Record the press
|
|
current_time = time.time()
|
|
timestamp = int((current_time - self.start_time) * 1000) # milliseconds
|
|
|
|
# Get all currently pressed keys
|
|
pressed_keys = sorted([k for k, v in self.key_states.items() if v])
|
|
|
|
# Add to raw presses
|
|
self.raw_presses.append((timestamp, pressed_keys.copy()))
|
|
|
|
# Update info
|
|
elapsed = current_time - self.start_time
|
|
self.info_label.config(text=f"Presses: {len(self.raw_presses)} | Time: {elapsed:.2f}s")
|
|
|
|
def on_key_release(self, key):
|
|
"""Handle key release event"""
|
|
if not self.is_recording:
|
|
return
|
|
|
|
self.key_states[key] = False
|
|
|
|
# Visual feedback
|
|
key_idx = int(key) - 1
|
|
self.key_buttons[key_idx].config(bg='white')
|
|
|
|
def start_recording(self):
|
|
"""Start recording mode"""
|
|
self.is_recording = True
|
|
self.recording_started = False
|
|
self.raw_presses = []
|
|
self.start_time = None
|
|
|
|
# Reset key states
|
|
for key in self.key_states:
|
|
self.key_states[key] = False
|
|
|
|
# Update UI
|
|
self.record_btn.config(state=tk.DISABLED)
|
|
self.stop_btn.config(state=tk.NORMAL)
|
|
self.save_btn.config(state=tk.DISABLED)
|
|
self.update_status("Waiting for first key press... Start playing!")
|
|
self.info_label.config(text="Steps: 0 | Time: 0.00s")
|
|
|
|
# Clear melody display
|
|
self.melody_text.config(state=tk.NORMAL)
|
|
self.melody_text.delete('1.0', tk.END)
|
|
self.melody_text.config(state=tk.DISABLED)
|
|
|
|
# Start metronome if manual tempo
|
|
if self.tempo_mode.get() == "MANUAL":
|
|
self.start_metronome()
|
|
|
|
def stop_recording(self):
|
|
"""Stop recording and process the melody"""
|
|
self.is_recording = False
|
|
self.recording_started = False
|
|
|
|
# Stop metronome
|
|
self.stop_metronome()
|
|
|
|
# Update UI
|
|
self.record_btn.config(state=tk.NORMAL)
|
|
self.stop_btn.config(state=tk.DISABLED)
|
|
self.save_btn.config(state=tk.NORMAL)
|
|
|
|
if not self.raw_presses:
|
|
self.update_status("No notes recorded!")
|
|
return
|
|
|
|
# Process the recording
|
|
self.update_status("Processing melody...")
|
|
self.process_melody()
|
|
|
|
def process_melody(self):
|
|
"""Process raw presses into tempo-quantized melody"""
|
|
if not self.raw_presses:
|
|
return
|
|
|
|
# Merge simultaneous presses (within chord detection window)
|
|
merged_presses = self.merge_simultaneous_presses(self.raw_presses, window=self.chord_window.get())
|
|
|
|
# Determine tempo
|
|
if self.tempo_mode.get() == "AUTO":
|
|
tempo = self.calculate_auto_tempo(merged_presses)
|
|
else:
|
|
tempo = self.manual_tempo.get()
|
|
|
|
# Quantize to steps
|
|
melody_steps = self.quantize_to_steps(merged_presses, tempo)
|
|
|
|
# Format output
|
|
melody_string = self.format_melody(melody_steps)
|
|
|
|
# Display results
|
|
output = f"Tempo: {tempo}ms\n"
|
|
output += f"Melody: {melody_string}\n"
|
|
output += f"\n--- Raw Recording Data (for troubleshooting) ---\n"
|
|
|
|
for timestamp, keys in merged_presses:
|
|
keys_str = '+'.join(keys) if keys else '0'
|
|
output += f"Button(s) {keys_str} pressed at: {timestamp}ms\n"
|
|
|
|
self.melody_text.config(state=tk.NORMAL)
|
|
self.melody_text.delete('1.0', tk.END)
|
|
self.melody_text.insert('1.0', output)
|
|
self.melody_text.config(state=tk.DISABLED)
|
|
|
|
self.update_status(f"✅ Melody processed! Tempo: {tempo}ms, Steps: {len(melody_steps)}")
|
|
|
|
# Store for saving
|
|
self.processed_tempo = tempo
|
|
self.processed_melody = melody_string
|
|
self.processed_raw = merged_presses
|
|
|
|
def merge_simultaneous_presses(self, presses, window=50):
|
|
"""Merge presses that occur within a time window (for chord detection)"""
|
|
if not presses:
|
|
return []
|
|
|
|
merged = []
|
|
i = 0
|
|
|
|
while i < len(presses):
|
|
current_time, current_keys = presses[i]
|
|
grouped_keys = set(current_keys)
|
|
|
|
# Look ahead for keys pressed within the window
|
|
j = i + 1
|
|
while j < len(presses) and presses[j][0] - current_time <= window:
|
|
grouped_keys.update(presses[j][1])
|
|
j += 1
|
|
|
|
# Add merged group
|
|
merged.append((current_time, sorted(list(grouped_keys))))
|
|
|
|
# Skip all merged presses
|
|
i = j
|
|
|
|
return merged
|
|
|
|
def calculate_auto_tempo(self, presses):
|
|
"""Calculate tempo using intelligent interval analysis"""
|
|
if len(presses) < 2:
|
|
return 100 # Default
|
|
|
|
# Get all intervals between consecutive presses
|
|
intervals = []
|
|
for i in range(1, len(presses)):
|
|
interval = presses[i][0] - presses[i-1][0]
|
|
if interval > 0:
|
|
intervals.append(interval)
|
|
|
|
print(f"\n=== DEBUG: Tempo Calculation ===")
|
|
print(f"Number of presses: {len(presses)}")
|
|
print(f"Intervals between presses: {intervals}")
|
|
|
|
if not intervals:
|
|
return 100
|
|
|
|
# Find GCD of all intervals
|
|
tempo_gcd = reduce(gcd, intervals)
|
|
print(f"GCD of intervals: {tempo_gcd}")
|
|
|
|
# Get unique intervals and their frequency
|
|
interval_counts = {}
|
|
for interval in intervals:
|
|
interval_counts[interval] = interval_counts.get(interval, 0) + 1
|
|
|
|
print(f"Interval frequency: {interval_counts}")
|
|
|
|
# Calculate average and median intervals
|
|
avg_interval = sum(intervals) / len(intervals)
|
|
sorted_intervals = sorted(intervals)
|
|
median_interval = sorted_intervals[len(sorted_intervals) // 2]
|
|
min_interval = min(intervals)
|
|
|
|
print(f"Average interval: {avg_interval:.2f}ms")
|
|
print(f"Median interval: {median_interval}ms")
|
|
print(f"Min interval: {min_interval}ms")
|
|
|
|
# SMARTER APPROACH: If GCD is tiny compared to actual intervals,
|
|
# use a clustering/rounding approach instead
|
|
|
|
if tempo_gcd < avg_interval * 0.1:
|
|
print(f"GCD too small ({tempo_gcd} < {avg_interval * 0.1:.2f}), using smarter detection...")
|
|
|
|
# Strategy: Find a tempo that minimizes total quantization error
|
|
# Try candidate tempos from min_interval down to reasonable values
|
|
|
|
best_tempo = min_interval
|
|
best_error = float('inf')
|
|
|
|
# Try different candidate tempos
|
|
for candidate in range(max(10, min_interval // 2), min_interval + 200, 10):
|
|
total_error = 0
|
|
for interval in intervals:
|
|
# Find nearest multiple
|
|
nearest_step = round(interval / candidate)
|
|
quantized_value = nearest_step * candidate
|
|
error = abs(interval - quantized_value)
|
|
total_error += error
|
|
|
|
avg_error = total_error / len(intervals)
|
|
print(f" Testing tempo {candidate}ms: avg error = {avg_error:.2f}ms")
|
|
|
|
if avg_error < best_error:
|
|
best_error = avg_error
|
|
best_tempo = candidate
|
|
|
|
tempo = best_tempo
|
|
print(f"Best tempo found: {tempo}ms with average error {best_error:.2f}ms")
|
|
|
|
else:
|
|
tempo = tempo_gcd
|
|
print(f"Using GCD as tempo: {tempo}ms")
|
|
|
|
# Apply user precision settings if specified
|
|
if self.precision_mode.get() == "MS":
|
|
precision = self.precision_ms.get()
|
|
# Round tempo to nearest multiple of precision
|
|
tempo = max(precision, round(tempo / precision) * precision)
|
|
print(f"After precision adjustment: {tempo}ms")
|
|
|
|
# Ensure tempo is reasonable (at least 10ms, at most 10s)
|
|
tempo = max(10, min(tempo, 10000))
|
|
print(f"Final tempo: {tempo}ms")
|
|
print(f"=== END DEBUG ===\n")
|
|
|
|
return tempo
|
|
|
|
def quantize_to_steps(self, presses, tempo):
|
|
"""Convert timestamps to tempo-based steps with pauses"""
|
|
if not presses:
|
|
return []
|
|
|
|
steps = []
|
|
last_step = 0
|
|
|
|
for timestamp, keys in presses:
|
|
# Calculate which step this press should be on
|
|
step = round(timestamp / tempo)
|
|
|
|
# Add pauses (0s) if needed
|
|
while last_step < step:
|
|
steps.append([])
|
|
last_step += 1
|
|
|
|
# Add the actual notes
|
|
steps.append(keys)
|
|
last_step = step + 1
|
|
|
|
return steps
|
|
|
|
def format_melody(self, steps):
|
|
"""Format steps into melody string"""
|
|
formatted = []
|
|
for step in steps:
|
|
if not step:
|
|
formatted.append('0')
|
|
elif len(step) == 1:
|
|
formatted.append(step[0])
|
|
else:
|
|
formatted.append('+'.join(step))
|
|
|
|
return ','.join(formatted)
|
|
|
|
def save_melody(self):
|
|
"""Save melody to file"""
|
|
if not hasattr(self, 'processed_melody'):
|
|
messagebox.showerror("Error", "No melody to save!")
|
|
return
|
|
|
|
filename = filedialog.asksaveasfilename(
|
|
defaultextension=".txt",
|
|
filetypes=[("Text files", "*.txt"), ("All files", "*.*")],
|
|
initialfile="melody.txt"
|
|
)
|
|
|
|
if not filename:
|
|
return
|
|
|
|
try:
|
|
with open(filename, 'w') as f:
|
|
f.write(f"Tempo: {self.processed_tempo}ms\n")
|
|
f.write(f"Melody: {self.processed_melody}\n")
|
|
f.write(f"\n--- Raw Recording Data (for troubleshooting) ---\n")
|
|
|
|
for timestamp, keys in self.processed_raw:
|
|
keys_str = '+'.join(keys) if keys else '0'
|
|
f.write(f"Button(s) {keys_str} pressed at: {timestamp}ms\n")
|
|
|
|
messagebox.showinfo("Success", f"Melody saved to {filename}")
|
|
self.update_status(f"✅ Saved to {filename}")
|
|
except Exception as e:
|
|
messagebox.showerror("Error", f"Failed to save: {str(e)}")
|
|
|
|
def start_metronome(self):
|
|
"""Start metronome ticking"""
|
|
if self.metronome_running:
|
|
return
|
|
|
|
self.metronome_running = True
|
|
self.metronome_thread = threading.Thread(target=self.metronome_loop, daemon=True)
|
|
self.metronome_thread.start()
|
|
|
|
def stop_metronome(self):
|
|
"""Stop metronome"""
|
|
self.metronome_running = False
|
|
self.metronome_indicator.config(text="")
|
|
|
|
def metronome_loop(self):
|
|
"""Metronome loop running in separate thread"""
|
|
tempo = self.manual_tempo.get()
|
|
beat = 0
|
|
|
|
while self.metronome_running:
|
|
# Visual indicator
|
|
if beat % 4 == 0:
|
|
self.metronome_indicator.config(text="♪", fg='#ff0000')
|
|
else:
|
|
self.metronome_indicator.config(text="♪", fg='#ffff00')
|
|
|
|
time.sleep(tempo / 1000.0)
|
|
self.metronome_indicator.config(text="")
|
|
time.sleep(0.05)
|
|
|
|
beat += 1
|
|
|
|
def update_status(self, message):
|
|
"""Update status label"""
|
|
self.status_label.config(text=message)
|
|
|
|
def main():
|
|
print("Starting PianoMaster 3000...")
|
|
print("Creating window...")
|
|
|
|
try:
|
|
root = tk.Tk()
|
|
print("Window created!")
|
|
|
|
print("Initializing app...")
|
|
app = PianoMaster3000(root)
|
|
print("App initialized!")
|
|
|
|
print("Starting main loop...")
|
|
root.mainloop()
|
|
print("Main loop ended.")
|
|
except Exception as e:
|
|
print(f"ERROR: {e}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
finally:
|
|
# Cleanup keyboard hooks
|
|
try:
|
|
keyboard.unhook_all()
|
|
print("Keyboard hooks cleaned up.")
|
|
except:
|
|
pass
|
|
|
|
if __name__ == "__main__":
|
|
print("PianoMaster 3000 - Starting...")
|
|
main()
|
|
print("PianoMaster 3000 - Exiting.")
|
|
input("\nPress ENTER to close...") |