Code Import from the last project
This commit is contained in:
642
main.py
Normal file
642
main.py
Normal file
@@ -0,0 +1,642 @@
|
||||
"""
|
||||
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...")
|
||||
Reference in New Issue
Block a user