disable graphics (the raspberry pi doesnt support drm, only a direct frame buffer)

This commit is contained in:
William Bell
2025-12-26 21:30:06 +00:00
parent faf15f6069
commit cfd83089c9

701
app.py
View File

@@ -9,68 +9,68 @@ import time
import os import os
from jelly import server, client from jelly import server, client
# --- Configuration Constants --- # # --- Configuration Constants ---
INITIAL_SCREEN_WIDTH = 240 # INITIAL_SCREEN_WIDTH = 240
INITIAL_SCREEN_HEIGHT = 240 # INITIAL_SCREEN_HEIGHT = 240
TARGET_FPS =60 # TARGET_FPS =60
# --- State Variables --- # # --- State Variables ---
state = { # state = {
"screen_width": INITIAL_SCREEN_WIDTH, # "screen_width": INITIAL_SCREEN_WIDTH,
"screen_height": INITIAL_SCREEN_HEIGHT, # "screen_height": INITIAL_SCREEN_HEIGHT,
} # }
# --- Utility Functions --- # # --- Utility Functions ---
def format_time_mm_ss(seconds): # def format_time_mm_ss(seconds):
"""Converts a time in seconds to an 'MM:SS' string format.""" # """Converts a time in seconds to an 'MM:SS' string format."""
seconds = int(seconds) # seconds = int(seconds)
minutes = seconds // 60 # minutes = seconds // 60
seconds_remainder = seconds % 60 # seconds_remainder = seconds % 60
return f"{minutes:02d}:{seconds_remainder:02d}" # return f"{minutes:02d}:{seconds_remainder:02d}"
def get_progress_bar_rect(screen_width, screen_height): # def get_progress_bar_rect(screen_width, screen_height):
width = screen_width # width = screen_width
height = screen_height*0.021 # height = screen_height*0.021
x = (screen_width - width) / 2 # x = (screen_width - width) / 2
y = screen_height - height # y = screen_height - height
return pr.Rectangle(x, y, width, height) # return pr.Rectangle(x, y, width, height)
def draw_progress_bar(rect, current_time, total_time): # def draw_progress_bar(rect, current_time, total_time):
if total_time > 0: # if total_time > 0:
progress_ratio = current_time / total_time # progress_ratio = current_time / total_time
else: # else:
progress_ratio = 0.0 # progress_ratio = 0.0
pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255)) # pr.draw_rectangle_rec(rect, pr.Color(100, 100, 100, 255))
progress_width = rect.width * progress_ratio # progress_width = rect.width * progress_ratio
pr.draw_rectangle( # pr.draw_rectangle(
int(rect.x), # int(rect.x),
int(rect.y)+1, # int(rect.y)+1,
int(progress_width), # int(progress_width),
int(rect.height), # int(rect.height),
pr.Color(200, 50, 50, 255), # pr.Color(200, 50, 50, 255),
) # )
# pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255)) # # pr.draw_rectangle_lines_ex(rect, 2, pr.Color(50, 50, 50, 255))
time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}" # time_text = f"{format_time_mm_ss(current_time)} / {format_time_mm_ss(total_time)}"
text_width = pr.measure_text(time_text, int(rect.height * 0.7)) # text_width = pr.measure_text(time_text, int(rect.height * 0.7))
# pr.draw_text( # # pr.draw_text(
# time_text, # # time_text,
# int(rect.x + rect.width / 2 - text_width / 2), # # int(rect.x + rect.width / 2 - text_width / 2),
# int(rect.y + rect.height * 0.15), # # int(rect.y + rect.height * 0.15),
# int(rect.height * 0.7), # # int(rect.height * 0.7),
# pr.WHITE, # # pr.WHITE,
# ) # # )
pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE) # pr.set_config_flags(pr.ConfigFlags.FLAG_WINDOW_RESIZABLE)
# pr.set_config_flags(pr.FLAG_MSAA_4X_HINT) # # pr.set_config_flags(pr.FLAG_MSAA_4X_HINT)
#pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE) # #pr.set_config_flags(pr.FLAG_FULLSCREEN_MODE)
pr.init_window(state["screen_width"], state["screen_height"], "UgPod") # pr.init_window(state["screen_width"], state["screen_height"], "UgPod")
pr.set_target_fps(TARGET_FPS) # pr.set_target_fps(TARGET_FPS)
player = GaplessPlayer() player = GaplessPlayer()
@@ -116,316 +116,323 @@ for track in tracks["Items"]:
) )
print("add queue done") print("add queue done")
player.load_state("data/player.json") # player.load_state("data/player.json")
close_event = threading.Event() # close_event = threading.Event()
def save_state_loop(): # def save_state_loop():
while not close_event.wait(10): # while not close_event.wait(10):
player.save_state("data/player.lock.json") # player.save_state("data/player.lock.json")
os.rename("data/player.lock.json", "data/player.json") # os.rename("data/player.lock.json", "data/player.json")
# save_state_thread = threading.Thread(target=save_state_loop) # # save_state_thread = threading.Thread(target=save_state_loop)
# save_state_thread.start() # # save_state_thread.start()
current_path = None # current_path = None
texture = None # texture = None
def load_texture(path): # def load_texture(path):
global texture, current_path # global texture, current_path
if not path: # if not path:
return # return
if path == current_path: # if path == current_path:
return # return
if texture is not None: # if texture is not None:
pr.unload_texture(texture) # pr.unload_texture(texture)
texture = pr.load_texture(path) # texture = pr.load_texture(path)
current_path = path # current_path = path
def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool: # def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool) -> bool:
clicked = False # clicked = False
rect = pr.Rectangle(pos.x, pos.y, size.x, size.y) # rect = pr.Rectangle(pos.x, pos.y, size.x, size.y)
# Optional hover background # # Optional hover background
if pr.check_collision_point_rec(pr.get_mouse_position(), rect): # if pr.check_collision_point_rec(pr.get_mouse_position(), rect):
pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4)) # pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4))
if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON): # if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON):
clicked = True # clicked = True
cx = pos.x + size.x / 2 # cx = pos.x + size.x / 2
cy = pos.y + size.y / 2 # cy = pos.y + size.y / 2
icon_padding = size.x * 0.25 # icon_padding = size.x * 0.25
icon_size = size.x - icon_padding * 2 # icon_size = size.x - icon_padding * 2
if is_playing: # if is_playing:
# PAUSE (two bars centered, same visual weight as play) # # PAUSE (two bars centered, same visual weight as play)
bar_width = icon_size * 0.25 # bar_width = icon_size * 0.25
bar_height = icon_size # bar_height = icon_size
left_x = cx - bar_width - bar_width * 0.4 # left_x = cx - bar_width - bar_width * 0.4
right_x = cx + bar_width * 0.4 # right_x = cx + bar_width * 0.4
top_y = 1+cy - bar_height / 2 # top_y = 1+cy - bar_height / 2
pr.draw_rectangle( # pr.draw_rectangle(
int(left_x), # int(left_x),
int(top_y), # int(top_y),
int(bar_width), # int(bar_width),
int(bar_height), # int(bar_height),
pr.WHITE, # pr.WHITE,
) # )
pr.draw_rectangle( # pr.draw_rectangle(
int(right_x), # int(right_x),
int(top_y), # int(top_y),
int(bar_width), # int(bar_width),
int(bar_height), # int(bar_height),
pr.WHITE, # pr.WHITE,
) # )
else: # else:
# PLAY (centered triangle) # # PLAY (centered triangle)
p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2) # p1 = pr.Vector2(cx - icon_size / 2, cy - icon_size / 2)
p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2) # p2 = pr.Vector2(cx - icon_size / 2, cy + icon_size / 2)
p3 = pr.Vector2(cx + icon_size / 2, cy) # p3 = pr.Vector2(cx + icon_size / 2, cy)
pr.draw_triangle(p1, p2, p3, pr.WHITE) # pr.draw_triangle(p1, p2, p3, pr.WHITE)
return clicked # return clicked
title = ScrollingText( # title = ScrollingText(
"", # "",
15 # 15
) # )
# --- Main Game Loop --- # # --- Main Game Loop ---
while not pr.window_should_close(): # while not pr.window_should_close():
# 1. Update # # 1. Update
current_width = pr.get_screen_width() # current_width = pr.get_screen_width()
current_height = pr.get_screen_height() # current_height = pr.get_screen_height()
if pr.is_key_pressed(pr.KEY_F11): # if pr.is_key_pressed(pr.KEY_F11):
pr.toggle_fullscreen() # pr.toggle_fullscreen()
if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE): # if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE):
if player.playing: # if player.playing:
player.pause() # player.pause()
else: # else:
player.play() # player.play()
if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT): # if pr.is_key_pressed(pr.KeyboardKey.KEY_LEFT):
player.seek(player.position - 5) # player.seek(player.position - 5)
if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT): # if pr.is_key_pressed(pr.KeyboardKey.KEY_RIGHT):
player.seek(player.position + 5) # player.seek(player.position + 5)
pr.begin_drawing() # pr.begin_drawing()
pr.clear_background(pr.Color(40, 40, 40, 255)) # pr.clear_background(pr.Color(40, 40, 40, 255))
dt = pr.get_frame_time() # dt = pr.get_frame_time()
progress_rect = get_progress_bar_rect(current_width, current_height) # progress_rect = get_progress_bar_rect(current_width, current_height)
# pr.draw_text( # # pr.draw_text(
# "UgPod", # # "UgPod",
# int(current_width * 0.05), # # int(current_width * 0.05),
# int(current_height * 0.05), # # int(current_height * 0.05),
# int(current_height * 0.05), # # int(current_height * 0.05),
# pr.SKYBLUE, # # pr.SKYBLUE,
# ) # # )
current_song = player.get_current_song() # current_song = player.get_current_song()
draw_progress_bar( # draw_progress_bar(
progress_rect, # progress_rect,
player.position, # player.position,
(current_song and current_song.duration) or 0.0, # (current_song and current_song.duration) or 0.0,
) # )
if current_song: # if current_song:
load_texture(current_song.album_cover_path) # load_texture(current_song.album_cover_path)
title_font_size = int(current_height*0.05) # title_font_size = int(current_height*0.05)
album_cover_size = int(min(current_width, current_height*0.7)) # album_cover_size = int(min(current_width, current_height*0.7))
title.speed = title_font_size*2.5 # title.speed = title_font_size*2.5
title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size) # title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size)
title.update(dt,title_size) # title.update(dt,title_size)
title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size) # title.set_text(f"{current_song.name} - {current_song.artist_name}", title_font_size)
title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size) # title.draw(pr.Vector2(int(current_height * 0.01),int(current_height * 0.8)),title_size)
# pr.draw_text( # # pr.draw_text(
# , # # ,
# , # # ,
# int(current_height * 0.03), # # int(current_height * 0.03),
# pr.WHITE, # # pr.WHITE,
# ) # # )
points = player.oscilloscope_data_points # points = player.oscilloscope_data_points
if texture is not None: # if texture is not None:
scale = min(album_cover_size / texture.width, album_cover_size / texture.height) # scale = min(album_cover_size / texture.width, album_cover_size / texture.height)
dest_rect = pr.Rectangle( # dest_rect = pr.Rectangle(
current_width//2 - album_cover_size//2, # current_width//2 - album_cover_size//2,
(current_height*0.8)//2 - album_cover_size//2, # (current_height*0.8)//2 - album_cover_size//2,
texture.width * scale, # texture.width * scale,
texture.height * scale, # texture.height * scale,
) # )
src_rect = pr.Rectangle(0, 0, texture.width, texture.height) # src_rect = pr.Rectangle(0, 0, texture.width, texture.height)
pr.draw_texture_pro( # pr.draw_texture_pro(
texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE # texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE
) # )
else: # else:
clip = pr.Rectangle(int(current_width//2 - album_cover_size//2), # clip = pr.Rectangle(int(current_width//2 - album_cover_size//2),
int((current_height*0.8)//2 - album_cover_size//2), # int((current_height*0.8)//2 - album_cover_size//2),
int(album_cover_size), # int(album_cover_size),
int(album_cover_size)) # int(album_cover_size))
pr.begin_scissor_mode( # pr.begin_scissor_mode(
int(clip.x), # int(clip.x),
int(clip.y), # int(clip.y),
int(clip.width), # int(clip.width),
int(clip.height), # int(clip.height),
) # )
pr.draw_rectangle( # pr.draw_rectangle(
int(clip.x), # int(clip.x),
int(clip.y), # int(clip.y),
int(clip.width), # int(clip.width),
int(clip.height), pr.BLACK) # int(clip.height), pr.BLACK)
# cx = current_width * 0.5+1 # # cx = current_width * 0.5+1
# cy = current_height * 0.4+1 # # cy = current_height * 0.4+1
# MAX_LEN = album_cover_size * 0.25 # tune this # # MAX_LEN = album_cover_size * 0.25 # tune this
# MIN_ALPHA = 10 # # MIN_ALPHA = 10
# MAX_ALPHA = 255 # # MAX_ALPHA = 255
# for i in range(len(points) - 1): # # for i in range(len(points) - 1):
# x1 = cx + points[i][0] * album_cover_size * 0.5 # # x1 = cx + points[i][0] * album_cover_size * 0.5
# y1 = cy + -points[i][1] * album_cover_size * 0.5 # # y1 = cy + -points[i][1] * album_cover_size * 0.5
# x2 = cx + points[i+1][0] * album_cover_size * 0.5 # # x2 = cx + points[i+1][0] * album_cover_size * 0.5
# y2 = cy + -points[i+1][1] * album_cover_size * 0.5 # # y2 = cy + -points[i+1][1] * album_cover_size * 0.5
# dx = x2 - x1 # # dx = x2 - x1
# dy = y2 - y1 # # dy = y2 - y1
# length = (dx * dx + dy * dy) ** 0.5 # # length = (dx * dx + dy * dy) ** 0.5
# # 1.0 = short line, 0.0 = long line # # # 1.0 = short line, 0.0 = long line
# t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2) # # t = max(0.0, min(1.0, 1.0 - (length / MAX_LEN)))*math.pow(i/len(points), 2)
# alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA)) # # alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA))
# color = pr.Color(255, 255, 255, alpha) # # color = pr.Color(255, 255, 255, alpha)
# pr.draw_line(int(x1), int(y1), int(x2), int(y2), color) # # pr.draw_line(int(x1), int(y1), int(x2), int(y2), color)
# draw background square # # draw background square
if len(points) >= 2: # if len(points) >= 2:
samples = np.fromiter( # samples = np.fromiter(
((p[0] + p[1]) * 0.5 for p in points), # ((p[0] + p[1]) * 0.5 for p in points),
dtype=np.float32 # dtype=np.float32
) # )
# Guard: FFT must have meaningful size # # Guard: FFT must have meaningful size
if samples.size > 128: # if samples.size > 128:
rect_x = int(current_width // 2 - album_cover_size // 2) # rect_x = int(current_width // 2 - album_cover_size // 2)
rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2) # rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2)
# ---- FFT ---- # # ---- FFT ----
FFT_SIZE = min(samples.size, 2048) # FFT_SIZE = min(samples.size, 2048)
window = np.hanning(FFT_SIZE) # window = np.hanning(FFT_SIZE)
fft = np.fft.rfft(samples[:FFT_SIZE] * window) # fft = np.fft.rfft(samples[:FFT_SIZE] * window)
magnitudes = np.abs(fft) # magnitudes = np.abs(fft)
# remove DC component (important for visuals) # # remove DC component (important for visuals)
magnitudes[0] = 0.0 # magnitudes[0] = 0.0
# ---- LOG BINNING ---- # # ---- LOG BINNING ----
num_bars = album_cover_size//10 # num_bars = album_cover_size//10
num_bins = magnitudes.size # num_bins = magnitudes.size
# logarithmic bin edges (low end stretched) # # logarithmic bin edges (low end stretched)
log_min = 1 # log_min = 1
log_max = math.log10(num_bins) # log_max = math.log10(num_bins)
log_edges = np.logspace( # log_edges = np.logspace(
math.log10(log_min), # math.log10(log_min),
log_max, # log_max,
num_bars + 1 # num_bars + 1
).astype(int) # ).astype(int)
bar_values = np.zeros(num_bars, dtype=np.float32)
for i in range(num_bars):
start = log_edges[i]
end = log_edges[i + 1]
if end <= start: # bar_values = np.zeros(num_bars, dtype=np.float32)
continue
bar_values[i] = np.mean(magnitudes[start:end])
# ---- STATIC SCALING ---- # for i in range(num_bars):
# start = log_edges[i]
# end = log_edges[i + 1]
# Instead of normalizing to the max of the frame, we scale by the FFT size. # if end <= start:
# For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps # continue
# maximum possible volume roughly to 1.0.
bar_values = bar_values / (FFT_SIZE / 4.0)
# ---- DRAW ---- # bar_values[i] = np.mean(magnitudes[start:end])
def map_to_screen(val): # # ---- STATIC SCALING ----
return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size
spacing = 0 # # Instead of normalizing to the max of the frame, we scale by the FFT size.
# # For a Hanning windowed FFT, dividing by (FFT_SIZE / 4) maps
# # maximum possible volume roughly to 1.0.
# bar_values = bar_values / (FFT_SIZE / 4.0)
for i in range(num_bars): # # ---- DRAW ----
# 1. Calculate integer pixel boundaries first
# This ensures the right edge of one bar is exactly the left edge of the next
x_start_int = int(map_to_screen(log_edges[i]))
x_end_int = int(map_to_screen(log_edges[i+1]))
# 2. Width is the difference between these fixed integer points # def map_to_screen(val):
w = (x_end_int - x_start_int) - spacing # return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size
value = bar_values[i] # spacing = 0
h = int(min(1.0, value) * album_cover_size)
# 3. Anchor to bottom # for i in range(num_bars):
y = (rect_y + album_cover_size) - h # # 1. Calculate integer pixel boundaries first
# # This ensures the right edge of one bar is exactly the left edge of the next
# x_start_int = int(map_to_screen(log_edges[i]))
# x_end_int = int(map_to_screen(log_edges[i+1]))
alpha = min(1.0, ((value+1)**2)-1) # # 2. Width is the difference between these fixed integer points
r = 255 # w = (x_end_int - x_start_int) - spacing
g = 0
b = 0
# Keep alpha at 255 (fully opaque) # value = bar_values[i]
color = pr.Color(r, g, b, int(255 * alpha)) # h = int(min(1.0, value) * album_cover_size)
# 4. Draw the bar # # 3. Anchor to bottom
# Use max(1, w) to ensure high-frequency bars don't disappear on small screens # y = (rect_y + album_cover_size) - h
pr.draw_rectangle(
x_start_int,
int(y),
max(1, int(w)),
h,
color
)
pr.end_scissor_mode()
pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height) # alpha = min(1.0, ((value+1)**2)-1)
size = pr.Vector2(current_height * 0.1, current_height * 0.1) # r = 255
# g = 0
# b = 0
if draw_play_pause_button(pos, size, player.playing): # # Keep alpha at 255 (fully opaque)
if player.playing: # color = pr.Color(r, g, b, int(255 * alpha))
player.pause()
else:
player.play()
pr.end_drawing()
# Cleanup # # 4. Draw the bar
if texture is not None: # # Use max(1, w) to ensure high-frequency bars don't disappear on small screens
pr.unload_texture(texture) # pr.draw_rectangle(
# x_start_int,
# int(y),
# max(1, int(w)),
# h,
# color
# )
# pr.end_scissor_mode()
pr.close_window() # pos = pr.Vector2(current_width * 0.5 - current_height * 0.05, current_height * 0.9-progress_rect.height)
close_event.set() # size = pr.Vector2(current_height * 0.1, current_height * 0.1)
# save_state_thread.join()
# if draw_play_pause_button(pos, size, player.playing):
# if player.playing:
# player.pause()
# else:
# player.play()
# pr.end_drawing()
# # Cleanup
# if texture is not None:
# pr.unload_texture(texture)
# pr.close_window()
# close_event.set()
# # save_state_thread.join()
player.play()
while True:
time.sleep(1)