Code Import from the last project

This commit is contained in:
2026-02-11 17:47:41 +02:00
parent df825a879f
commit 8677b017ec

642
main.py Normal file
View 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...")