update
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 7.7 KiB |
@@ -1,8 +1,12 @@
|
||||
"""Generate the app icon: green rounded square with a modern music note."""
|
||||
"""Generate the app icon: olive-green rounded square with a single elegant note."""
|
||||
import struct
|
||||
import zlib
|
||||
import math
|
||||
import os
|
||||
|
||||
BG_R, BG_G, BG_B = 124, 154, 60
|
||||
|
||||
|
||||
def create_icon():
|
||||
sizes = [16, 32, 48, 64, 128, 256]
|
||||
images = []
|
||||
@@ -14,17 +18,13 @@ def create_icon():
|
||||
|
||||
|
||||
def _render(size):
|
||||
"""Render a green rounded-square with a music note as RGBA pixels."""
|
||||
"""Render an olive-green rounded-square with a single music note."""
|
||||
pixels = bytearray(size * size * 4)
|
||||
cx, cy = size / 2, size / 2
|
||||
r = size * 0.42
|
||||
corner = size * 0.22
|
||||
|
||||
for y in range(size):
|
||||
for x in range(size):
|
||||
i = (y * size + x) * 4
|
||||
dx = x - cx
|
||||
dy = y - cy
|
||||
|
||||
inside = _rounded_rect(x, y, size, corner)
|
||||
|
||||
@@ -32,29 +32,29 @@ def _render(size):
|
||||
nr, ng, nb, na = _note_pixel(x, y, size)
|
||||
if na > 0:
|
||||
pixels[i] = nr
|
||||
pixels[i+1] = ng
|
||||
pixels[i+2] = nb
|
||||
pixels[i+3] = na
|
||||
pixels[i + 1] = ng
|
||||
pixels[i + 2] = nb
|
||||
pixels[i + 3] = na
|
||||
else:
|
||||
pixels[i] = 29
|
||||
pixels[i+1] = 185
|
||||
pixels[i+2] = 84
|
||||
pixels[i+3] = 255
|
||||
pixels[i] = BG_R
|
||||
pixels[i + 1] = BG_G
|
||||
pixels[i + 2] = BG_B
|
||||
pixels[i + 3] = 255
|
||||
elif inside > 0:
|
||||
a = int(inside * 255)
|
||||
nr, ng, nb, na = _note_pixel(x, y, size)
|
||||
if na > 0:
|
||||
pixels[i] = nr
|
||||
pixels[i+1] = ng
|
||||
pixels[i+2] = nb
|
||||
pixels[i+3] = min(255, int(na * inside))
|
||||
pixels[i + 1] = ng
|
||||
pixels[i + 2] = nb
|
||||
pixels[i + 3] = min(255, int(na * inside))
|
||||
else:
|
||||
pixels[i] = 29
|
||||
pixels[i+1] = 185
|
||||
pixels[i+2] = 84
|
||||
pixels[i+3] = a
|
||||
pixels[i] = BG_R
|
||||
pixels[i + 1] = BG_G
|
||||
pixels[i + 2] = BG_B
|
||||
pixels[i + 3] = a
|
||||
else:
|
||||
pixels[i:i+4] = b'\x00\x00\x00\x00'
|
||||
pixels[i:i + 4] = b'\x00\x00\x00\x00'
|
||||
|
||||
return bytes(pixels)
|
||||
|
||||
@@ -98,54 +98,61 @@ def _rounded_rect(x, y, size, corner):
|
||||
|
||||
|
||||
def _note_pixel(x, y, size):
|
||||
"""Draw a modern double-beamed music note (two note heads + beam)."""
|
||||
"""Draw a single elegant eighth note (quaver)."""
|
||||
s = size
|
||||
white = (255, 255, 255, 240)
|
||||
white = (255, 255, 255, 235)
|
||||
|
||||
head1_cx = s * 0.33
|
||||
head1_cy = s * 0.68
|
||||
head2_cx = s * 0.60
|
||||
head2_cy = s * 0.75
|
||||
# --- Note head: tilted filled ellipse ---
|
||||
hcx = s * 0.44
|
||||
hcy = s * 0.71
|
||||
hrx = s * 0.14
|
||||
hry = s * 0.09
|
||||
tilt = -0.45
|
||||
|
||||
head_rx = s * 0.10
|
||||
head_ry = s * 0.07
|
||||
ca, sa = math.cos(tilt), math.sin(tilt)
|
||||
dx, dy = x - hcx, y - hcy
|
||||
u = (dx * ca + dy * sa) / hrx
|
||||
v = (-dx * sa + dy * ca) / hry
|
||||
d2 = u * u + v * v
|
||||
|
||||
dx1 = (x - head1_cx) / head_rx
|
||||
dy1 = (y - head1_cy) / head_ry
|
||||
if dx1 * dx1 + dy1 * dy1 <= 1.0:
|
||||
if d2 <= 1.0:
|
||||
return white
|
||||
if d2 <= 1.25:
|
||||
alpha = max(0, int(235 * (1.25 - d2) / 0.25))
|
||||
if alpha > 10:
|
||||
return (255, 255, 255, alpha)
|
||||
|
||||
# --- Stem: thin vertical line from head to top ---
|
||||
stem_x = hcx + hrx * ca
|
||||
stem_hw = max(0.6, s * 0.018)
|
||||
stem_top = s * 0.19
|
||||
stem_bot = hcy - hry * 0.15
|
||||
|
||||
if stem_top <= y <= stem_bot and abs(x - stem_x) <= stem_hw:
|
||||
edge = stem_hw - abs(x - stem_x)
|
||||
if edge < 1.0:
|
||||
return (255, 255, 255, max(0, int(235 * edge)))
|
||||
return white
|
||||
|
||||
dx2 = (x - head2_cx) / head_rx
|
||||
dy2 = (y - head2_cy) / head_ry
|
||||
if dx2 * dx2 + dy2 * dy2 <= 1.0:
|
||||
return white
|
||||
# --- Flag: elegant curved shape at top of stem ---
|
||||
ft = stem_top
|
||||
fh = s * 0.32
|
||||
fb = ft + fh
|
||||
fw = s * 0.19
|
||||
|
||||
stem_w = max(1, s * 0.04)
|
||||
stem1_x = head1_cx + head_rx - stem_w / 2
|
||||
stem2_x = head2_cx + head_rx - stem_w / 2
|
||||
if ft <= y <= fb and x >= stem_x - stem_hw:
|
||||
t = (y - ft) / fh
|
||||
bulge = fw * math.sin(t * math.pi * 0.82) * ((1 - t) ** 0.65)
|
||||
right_edge = stem_x + stem_hw + bulge
|
||||
|
||||
stem_top = s * 0.22
|
||||
stem1_bot = head1_cy
|
||||
stem2_bot = head2_cy
|
||||
|
||||
if (abs(x - stem1_x) <= stem_w and stem_top <= y <= stem1_bot):
|
||||
return white
|
||||
if (abs(x - stem2_x) <= stem_w and stem_top + s * 0.07 <= y <= stem2_bot):
|
||||
return white
|
||||
|
||||
beam_h = max(1, s * 0.045)
|
||||
for bi, beam_y_base in enumerate([stem_top, stem_top + beam_h * 2.5]):
|
||||
left_x = stem1_x
|
||||
right_x = stem2_x
|
||||
left_y = beam_y_base
|
||||
right_y = beam_y_base + s * 0.07
|
||||
|
||||
if left_x <= x <= right_x:
|
||||
t = (x - left_x) / max(1, right_x - left_x)
|
||||
beam_top = left_y + t * (right_y - left_y)
|
||||
beam_bot = beam_top + beam_h
|
||||
if beam_top <= y <= beam_bot:
|
||||
return white
|
||||
if x <= right_edge:
|
||||
edge_dist = right_edge - x
|
||||
if edge_dist < 1.2 and bulge > stem_hw:
|
||||
alpha = max(0, int(235 * edge_dist / 1.2))
|
||||
if alpha > 10:
|
||||
return (255, 255, 255, alpha)
|
||||
return (0, 0, 0, 0)
|
||||
return white
|
||||
|
||||
return (0, 0, 0, 0)
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@ ES_AWAYMODE_REQUIRED = 0x00000040
|
||||
BG = "#0b0b0b"
|
||||
CARD = "#151515"
|
||||
BORDER = "#1e1e1e"
|
||||
GREEN = "#1DB954"
|
||||
GREEN_H = "#1ed760"
|
||||
GREEN = "#7C9A3C"
|
||||
GREEN_H = "#90AE4A"
|
||||
RED = "#e74c3c"
|
||||
RED_H = "#ff6b6b"
|
||||
ORANGE = "#f59e0b"
|
||||
@@ -222,8 +222,6 @@ class SpotifyRecorderApp:
|
||||
|
||||
self.root = ctk.CTk()
|
||||
self.root.title("Surovy's Music Recorder")
|
||||
self.root.geometry("1400x880")
|
||||
self.root.minsize(1000, 620)
|
||||
self.root.configure(fg_color=BG)
|
||||
|
||||
ico = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
@@ -233,9 +231,78 @@ class SpotifyRecorderApp:
|
||||
self.root.after(200, lambda: self.root.iconbitmap(ico))
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
# state
|
||||
self.platform_name = "Spotify"
|
||||
self.platform_cfg = PLATFORMS["Spotify"]
|
||||
self._app_initialized = False
|
||||
self._show_selector()
|
||||
|
||||
# ── platform selector ─────────────────────────────────────
|
||||
|
||||
_PLATFORM_ICONS = {
|
||||
"Spotify": "\u266a",
|
||||
"Apple Music": "\u266a",
|
||||
"Amazon Music": "\u266a",
|
||||
"Tidal": "\u266a",
|
||||
"Deezer": "\u266a",
|
||||
"YouTube Music": "\u25b6",
|
||||
"TuneIn / Radio": "\U0001f4fb",
|
||||
"SoundCloud": "\u2601",
|
||||
"System Audio": "\U0001f50a",
|
||||
}
|
||||
|
||||
def _show_selector(self):
|
||||
self.root.geometry("680x530")
|
||||
self.root.minsize(680, 530)
|
||||
self.root.resizable(False, False)
|
||||
|
||||
self._sel = ctk.CTkFrame(self.root, fg_color=BG)
|
||||
self._sel.pack(fill="both", expand=True)
|
||||
|
||||
hdr = ctk.CTkFrame(self._sel, fg_color="transparent")
|
||||
hdr.pack(pady=(38, 0))
|
||||
|
||||
icon_box = ctk.CTkFrame(hdr, width=54, height=54, fg_color=GREEN,
|
||||
corner_radius=15)
|
||||
icon_box.pack()
|
||||
icon_box.pack_propagate(False)
|
||||
ctk.CTkLabel(icon_box, text="\u266a",
|
||||
font=ctk.CTkFont(size=24, weight="bold"),
|
||||
text_color="white").pack(expand=True)
|
||||
|
||||
ctk.CTkLabel(hdr, text="Surovy's Music Recorder",
|
||||
font=ctk.CTkFont(size=22, weight="bold"),
|
||||
text_color=TXT).pack(pady=(14, 4))
|
||||
ctk.CTkLabel(hdr, text="Dienst auswaehlen",
|
||||
font=ctk.CTkFont(size=13), text_color=TXT2).pack()
|
||||
|
||||
grid = ctk.CTkFrame(self._sel, fg_color="transparent")
|
||||
grid.pack(pady=(28, 0))
|
||||
|
||||
for i, name in enumerate(PLATFORM_NAMES):
|
||||
row, col = divmod(i, 3)
|
||||
ic = self._PLATFORM_ICONS.get(name, "\u266a")
|
||||
btn = ctk.CTkButton(
|
||||
grid, text=f"{ic} {name}",
|
||||
width=195, height=64, corner_radius=14,
|
||||
font=ctk.CTkFont(size=13, weight="bold"),
|
||||
fg_color=CARD, hover_color="#252525",
|
||||
border_width=1, border_color=BORDER,
|
||||
text_color=TXT,
|
||||
command=lambda n=name: self._on_platform_selected(n),
|
||||
)
|
||||
btn.grid(row=row, column=col, padx=8, pady=8)
|
||||
|
||||
ctk.CTkLabel(self._sel,
|
||||
text="Du kannst den Dienst spaeter jederzeit wechseln.",
|
||||
font=ctk.CTkFont(size=10), text_color=DIM
|
||||
).pack(pady=(22, 0))
|
||||
|
||||
def _on_platform_selected(self, name):
|
||||
self._sel.destroy()
|
||||
self.root.resizable(True, True)
|
||||
self.root.geometry("1400x880")
|
||||
self.root.minsize(1000, 620)
|
||||
|
||||
self.platform_name = name
|
||||
self.platform_cfg = PLATFORMS[name]
|
||||
self.recorder = AudioRecorder()
|
||||
self.spotify_hwnd = None
|
||||
self.original_style = None
|
||||
@@ -266,6 +333,11 @@ class SpotifyRecorderApp:
|
||||
self._restart_after_id = None
|
||||
self._restart_fails = 0
|
||||
self._lock = threading.Lock()
|
||||
self._silence_start = None
|
||||
self._silence_threshold = 0.012
|
||||
self._min_silence_secs = 2.0
|
||||
|
||||
self._app_initialized = True
|
||||
|
||||
self._build_controls()
|
||||
self._build_embed_area()
|
||||
@@ -300,7 +372,7 @@ class SpotifyRecorderApp:
|
||||
hcol = ctk.CTkFrame(hdr, fg_color="transparent")
|
||||
hcol.pack(side="left", padx=10)
|
||||
ctk.CTkLabel(hcol, text="Surovy's",
|
||||
font=ctk.CTkFont(size=10), text_color=GREEN).pack(anchor="w")
|
||||
font=ctk.CTkFont(size=10), text_color=TXT).pack(anchor="w")
|
||||
ctk.CTkLabel(hcol, text="Music Recorder",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
text_color=TXT).pack(anchor="w")
|
||||
@@ -311,7 +383,7 @@ class SpotifyRecorderApp:
|
||||
ctk.CTkLabel(src_row, text="QUELLE",
|
||||
font=ctk.CTkFont(size=9, weight="bold"),
|
||||
text_color=DIM).pack(side="left")
|
||||
self.platform_var = ctk.StringVar(value="Spotify")
|
||||
self.platform_var = ctk.StringVar(value=self.platform_name)
|
||||
ctk.CTkOptionMenu(
|
||||
src_row, values=PLATFORM_NAMES,
|
||||
variable=self.platform_var, width=170, height=28,
|
||||
@@ -1008,6 +1080,13 @@ class SpotifyRecorderApp:
|
||||
# ── track monitoring ───────────────────────────────────────
|
||||
|
||||
def _tick_monitor(self):
|
||||
try:
|
||||
self._tick_monitor_inner()
|
||||
except Exception:
|
||||
pass
|
||||
self.root.after(500, self._tick_monitor)
|
||||
|
||||
def _tick_monitor_inner(self):
|
||||
sep = self.platform_cfg.get("sep")
|
||||
pause_titles = self.platform_cfg.get("pause", set())
|
||||
|
||||
@@ -1017,11 +1096,7 @@ class SpotifyRecorderApp:
|
||||
except Exception:
|
||||
title = ""
|
||||
|
||||
if self.active and self.current_track is None and self.is_playing:
|
||||
self.recorder.harvest_frames()
|
||||
|
||||
if not title:
|
||||
self.root.after(500, self._tick_monitor)
|
||||
return
|
||||
|
||||
if sep and sep in title:
|
||||
@@ -1045,6 +1120,20 @@ class SpotifyRecorderApp:
|
||||
parts = title.split(sep, 1)
|
||||
self.artist_lbl.configure(text=parts[0].strip())
|
||||
self.title_lbl.configure(text=parts[1].strip())
|
||||
elif (self.active and self.current_track is None
|
||||
and title == self.last_title):
|
||||
parts = title.split(sep, 1)
|
||||
artist = parts[0].strip()
|
||||
song = parts[1].strip()
|
||||
key = self._song_key(artist, song)
|
||||
if key not in self.recorded_songs:
|
||||
self.track_number += 1
|
||||
self.current_track = {
|
||||
"artist": artist, "title": song}
|
||||
self._update_track_ui()
|
||||
self._set_status("AUFNAHME", GREEN)
|
||||
else:
|
||||
self.recorder.harvest_frames()
|
||||
self.last_title = title
|
||||
|
||||
else:
|
||||
@@ -1080,7 +1169,7 @@ class SpotifyRecorderApp:
|
||||
self.title_lbl.configure(
|
||||
text="wird nicht aufgenommen")
|
||||
self.tnum_lbl.configure(text="")
|
||||
if self.active:
|
||||
if self.active and self.current_track is None:
|
||||
self.recorder.harvest_frames()
|
||||
self.pause_since = None
|
||||
|
||||
@@ -1107,9 +1196,34 @@ class SpotifyRecorderApp:
|
||||
text=lbl, text_color=ORANGE)
|
||||
|
||||
elif not self.platform_cfg.get("process"):
|
||||
pass
|
||||
|
||||
self.root.after(500, self._tick_monitor)
|
||||
if self.active:
|
||||
lvl = self.recorder.get_level()
|
||||
if lvl < self._silence_threshold:
|
||||
if self._silence_start is None:
|
||||
self._silence_start = time.time()
|
||||
elif (time.time() - self._silence_start
|
||||
>= self._min_silence_secs
|
||||
and self.current_track):
|
||||
with self._lock:
|
||||
frames = self.recorder.harvest_frames()
|
||||
if frames:
|
||||
num = self.track_number
|
||||
trk = dict(self.current_track)
|
||||
threading.Thread(
|
||||
target=self._save,
|
||||
args=(frames, trk, num),
|
||||
daemon=True).start()
|
||||
self.track_number += 1
|
||||
self.current_track = {
|
||||
"artist": self.platform_name,
|
||||
"title": f"Track {self.track_number:02d}",
|
||||
}
|
||||
self._update_track_ui()
|
||||
self.rec_start = time.time()
|
||||
self._silence_start = None
|
||||
self._set_status("NEUER TRACK", GREEN)
|
||||
else:
|
||||
self._silence_start = None
|
||||
|
||||
def _handle_track_change(self, new_title):
|
||||
sep = self.platform_cfg.get("sep", " - ")
|
||||
@@ -1268,7 +1382,7 @@ class SpotifyRecorderApp:
|
||||
if self.active:
|
||||
self._pulse_on = not self._pulse_on
|
||||
self.dot_lbl.configure(
|
||||
text_color=GREEN if self._pulse_on else "#14532d")
|
||||
text_color=GREEN if self._pulse_on else "#3E4D1E")
|
||||
self.root.after(650, self._tick_pulse)
|
||||
|
||||
def _tick_alive(self):
|
||||
@@ -1298,6 +1412,19 @@ class SpotifyRecorderApp:
|
||||
anchor="center")
|
||||
if self.active:
|
||||
self._stop()
|
||||
|
||||
if self.active and self.recorder._stream:
|
||||
try:
|
||||
if not self.recorder._stream.is_active():
|
||||
self.recorder.stop()
|
||||
try:
|
||||
self.recorder.start()
|
||||
self._prevent_sleep()
|
||||
except Exception:
|
||||
self._stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.root.after(3000, self._tick_alive)
|
||||
|
||||
# ── statistics ─────────────────────────────────────────────
|
||||
@@ -1349,6 +1476,9 @@ class SpotifyRecorderApp:
|
||||
# ── cleanup ────────────────────────────────────────────────
|
||||
|
||||
def _on_close(self):
|
||||
if not self._app_initialized:
|
||||
self.root.destroy()
|
||||
return
|
||||
self._user_stopped = True
|
||||
if self._restart_after_id:
|
||||
self.root.after_cancel(self._restart_after_id)
|
||||
|
||||
Reference in New Issue
Block a user