change the layout and fix f string bug on raspberry pi python 3

This commit is contained in:
William Bell
2025-12-26 19:47:30 +00:00
parent d069150c54
commit d86587e526
6 changed files with 371 additions and 253 deletions

392
app.py
View File

@@ -1,27 +1,23 @@
import pyray as pr
import math
from ctypes import c_float
from gapless_player import GaplessPlayer, song_data_to_Song, server, client, Song
from gapless_player import GaplessPlayer, Song, song_data_to_Song
from scrolling_text import ScrollingText
import numpy as np
import threading
import time
import os
from jelly import server, client
# --- Configuration Constants ---
INITIAL_SCREEN_WIDTH = 240
INITIAL_SCREEN_HEIGHT = 240
TARGET_FPS = 60
TARGET_FPS =60
# --- State Variables ---
state = {
"screen_width": INITIAL_SCREEN_WIDTH,
"screen_height": INITIAL_SCREEN_HEIGHT,
"current_time": 120.0,
"total_time": 300.0,
"is_playing": True,
# 3D Camera State
"camera": None,
"render_texture": None,
# Assets
"album_texture": None,
"album_model": None,
}
# --- Utility Functions ---
@@ -37,7 +33,7 @@ def format_time_mm_ss(seconds):
def get_progress_bar_rect(screen_width, screen_height):
width = screen_width
height = 5
height = screen_height*0.021
x = (screen_width - width) / 2
y = screen_height - height
return pr.Rectangle(x, y, width, height)
@@ -53,7 +49,7 @@ def draw_progress_bar(rect, current_time, total_time):
progress_width = rect.width * progress_ratio
pr.draw_rectangle(
int(rect.x),
int(rect.y),
int(rect.y)+1,
int(progress_width),
int(rect.height),
pr.Color(200, 50, 50, 255),
@@ -70,10 +66,9 @@ def draw_progress_bar(rect, current_time, total_time):
# pr.WHITE,
# )
# Initialization
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.init_window(state["screen_width"], state["screen_height"], "UgPod")
pr.set_target_fps(TARGET_FPS)
@@ -81,149 +76,54 @@ player = GaplessPlayer()
print("add queue")
# player.add_to_queue(build_jellyfin_audio_url(server["address"], , server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("66559c40d5904944a3f97198d0297894")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("f37982227942d3df031381e653ec5790")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c")["Id"], server["AccessToken"], server["UserId"]))
print(client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"))
# player.add_to_queue(
# Song(
# "music/pink floyd/dark side of the moon/01 Speak to Me.flac",
# "Speak to Me",
# "The Dark Side Of The Moon",
# "music/albumcover.png",
# "Pink Floyd",
# "/mnt/HDD/Downloads/72 Pantera.wav",
# "Bullseye",
# 1,
# "KDrew",
# "",
# "KDrew",
# )
# )
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7"), server
)
albums = client.jellyfin.user_items(
params={
"IncludeItemTypes": "MusicAlbum",
"SearchTerm": "Dawn FM", # album name
"Recursive": True,
},
)
album = albums["Items"][0] # pick the album you want
album_id = album["Id"]
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("dab6efb24bb2372794d2b4fb53a12376"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("58822c0fc47ec63ba798ba4f04ea3cf3"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("6382005f9dbae8d187d80a5cdca3e7a6"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("a5d2453e07a4998ea20e957c44f90be6"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("398d481a7b85287ad200578b5ab997b0"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("f9f32ca67be7f83139cee3c66e1e4965"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("2f651e103b1fd22ea2f202d6f3398b36"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("164b95968ab1a725fff060fa8c351cc8"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("38a6c21561f54d284a6acad89a3ea8b0"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("631aeddb0557fef65f49463abb20ad7f"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("3d611c8664c5b2072edbf46da2a76c89"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("66559c40d5904944a3f97198d0297894"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("84b75eeb5c8e862d002bae05d2671b1b"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("7ef66992426093252696e1d8666a22e4"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("f37982227942d3df031381e653ec5790"), server
)
)
player.add_to_queue(
song_data_to_Song(
client.jellyfin.get_item("0e8fc5fcf119de0439f5a15a4f255c5c"), server
)
tracks = client.jellyfin.user_items(
params={
"ParentId": album_id,
"IncludeItemTypes": "Audio",
"SortBy": "IndexNumber",
"SortOrder": "Ascending",
},
)
# player.add_to_queue('music/pink floyd/dark side of the moon/01 Speak to Me.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("99067e877d91be1a66eb5a7ff2f4128f")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue('music/pink floyd/dark side of the moon/02 Breathe (In the Air).flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("916eda422f48efd8705f29e0600a3e60")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue('music/pink floyd/dark side of the moon/03 On the Run.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("5e1067d59ed98979ad12a58548b27b83")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue('music/pink floyd/dark side of the moon/04 Time.flac')#(build_jellyfin_audio_url(server["address"], client.jellyfin.get_item("8bcf8240d12aa5c3b14dc3b57f32fef7")["Id"], server["AccessToken"], server["UserId"]))
# player.add_to_queue('music/pink floyd/dark side of the moon/05 The Great Gig in the Sky.flac')
# player.add_to_queue('music/pink floyd/dark side of the moon/06 Money.flac')
# player.add_to_queue('music/pink floyd/dark side of the moon/07 Us and Them.flac')
# player.add_to_queue('music/pink floyd/dark side of the moon/08 Any Colour You Like.flac')
# player.add_to_queue('music/pink floyd/dark side of the moon/09 Brain Damage.flac')
# player.add_to_queue('music/pink floyd/dark side of the moon/10 Eclipse.flac')
for track in tracks["Items"]:
player.add_to_queue(
song_data_to_Song(
track, server
)
)
print("add queue done")
player.load_state("data/player.json")
close_event = threading.Event()
def save_state_loop():
while not close_event.wait(10):
player.save_state("data/player.lock.json")
os.rename("data/player.lock.json", "data/player.json")
# save_state_thread = threading.Thread(target=save_state_loop)
# save_state_thread.start()
current_path = None
texture = None
@@ -255,7 +155,6 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool)
pr.draw_rectangle_rec(rect, pr.fade(pr.BLACK, 0.4))
if pr.is_mouse_button_pressed(pr.MOUSE_LEFT_BUTTON):
clicked = True
cx = pos.x + size.x / 2
cy = pos.y + size.y / 2
@@ -269,7 +168,7 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool)
left_x = cx - bar_width - bar_width * 0.4
right_x = cx + bar_width * 0.4
top_y = cy - bar_height / 2
top_y = 1+cy - bar_height / 2
pr.draw_rectangle(
int(left_x),
@@ -297,10 +196,8 @@ def draw_play_pause_button(pos: pr.Vector2, size: pr.Vector2, is_playing: bool)
title = ScrollingText(
text="Aphex Twin - Xtal",
font_size=15,
speed=35,
pause_time=1.2,
"",
15
)
# --- Main Game Loop ---
@@ -309,6 +206,8 @@ while not pr.window_should_close():
current_width = pr.get_screen_width()
current_height = pr.get_screen_height()
if pr.is_key_pressed(pr.KEY_F11):
pr.toggle_fullscreen()
if pr.is_key_pressed(pr.KeyboardKey.KEY_SPACE):
if player.playing:
player.pause()
@@ -325,41 +224,44 @@ while not pr.window_should_close():
progress_rect = get_progress_bar_rect(current_width, current_height)
pr.draw_text(
"UgPod",
int(current_width * 0.05),
int(current_height * 0.05),
int(current_height * 0.05),
pr.SKYBLUE,
)
# pr.draw_text(
# "UgPod",
# int(current_width * 0.05),
# int(current_height * 0.05),
# int(current_height * 0.05),
# pr.SKYBLUE,
# )
current_song = player.get_current_song()
max_size = int(current_height * 0.075)
draw_progress_bar(
progress_rect,
player.position,
(current_song and current_song.duration) or 0.0,
)
if current_song:
load_texture(current_song.album_cover_path)
title_size = pr.Vector2(current_width-int(current_width * 0.05 + max_size * 1.1), 30)
title_font_size = int(current_height*0.05)
album_cover_size = int(min(current_width, current_height*0.7))
title.speed = title_font_size*2.5
title_size = pr.Vector2(current_width-int(current_height * 0.01)*2, title_font_size)
title.update(dt,title_size)
title.set_text(f"{current_song.name} - {current_song.artist_name}")
title.draw(pr.Vector2(int(current_width * 0.05 + max_size * 1.1),int(current_height * 0.8)),title_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)
# pr.draw_text(
# ,
# ,
# int(current_height * 0.03),
# pr.WHITE,
# )
draw_progress_bar(
progress_rect,
player.position,
current_song.duration,
)
points = player.oscilloscope_data_points
if texture is not None:
scale = min(max_size / texture.width, max_size / texture.height)
scale = min(album_cover_size / texture.width, album_cover_size / texture.height)
dest_rect = pr.Rectangle(
int(current_width * 0.05),
int(current_height * 0.8),
current_width//2 - album_cover_size//2,
(current_height*0.8)//2 - album_cover_size//2,
texture.width * scale,
texture.height * scale,
)
@@ -369,6 +271,146 @@ while not pr.window_should_close():
pr.draw_texture_pro(
texture, src_rect, dest_rect, pr.Vector2(0, 0), 0.0, pr.WHITE
)
else:
clip = pr.Rectangle(int(current_width//2 - album_cover_size//2),
int((current_height*0.8)//2 - album_cover_size//2),
int(album_cover_size),
int(album_cover_size))
pr.begin_scissor_mode(
int(clip.x),
int(clip.y),
int(clip.width),
int(clip.height),
)
pr.draw_rectangle(
int(clip.x),
int(clip.y),
int(clip.width),
int(clip.height), pr.BLACK)
# cx = current_width * 0.5+1
# cy = current_height * 0.4+1
# MAX_LEN = album_cover_size * 0.25 # tune this
# MIN_ALPHA = 10
# MAX_ALPHA = 255
# for i in range(len(points) - 1):
# x1 = cx + points[i][0] * 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
# y2 = cy + -points[i+1][1] * album_cover_size * 0.5
# dx = x2 - x1
# dy = y2 - y1
# length = (dx * dx + dy * dy) ** 0.5
# # 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)
# alpha = int(MIN_ALPHA + t * (MAX_ALPHA - MIN_ALPHA))
# color = pr.Color(255, 255, 255, alpha)
# pr.draw_line(int(x1), int(y1), int(x2), int(y2), color)
# draw background square
if len(points) >= 2:
samples = np.fromiter(
((p[0] + p[1]) * 0.5 for p in points),
dtype=np.float32
)
# Guard: FFT must have meaningful size
if samples.size > 128:
rect_x = int(current_width // 2 - album_cover_size // 2)
rect_y = int((current_height * 0.8) // 2 - album_cover_size // 2)
# ---- FFT ----
FFT_SIZE = min(samples.size, 2048)
window = np.hanning(FFT_SIZE)
fft = np.fft.rfft(samples[:FFT_SIZE] * window)
magnitudes = np.abs(fft)
# remove DC component (important for visuals)
magnitudes[0] = 0.0
# ---- LOG BINNING ----
num_bars = album_cover_size//10
num_bins = magnitudes.size
# logarithmic bin edges (low end stretched)
log_min = 1
log_max = math.log10(num_bins)
log_edges = np.logspace(
math.log10(log_min),
log_max,
num_bars + 1
).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:
continue
bar_values[i] = np.mean(magnitudes[start:end])
# ---- STATIC SCALING ----
# 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)
# ---- DRAW ----
def map_to_screen(val):
return rect_x + (math.log10(max(1, val)) / log_max) * album_cover_size
spacing = 0
for i in range(num_bars):
# 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
w = (x_end_int - x_start_int) - spacing
value = bar_values[i]
h = int(min(1.0, value) * album_cover_size)
# 3. Anchor to bottom
y = (rect_y + album_cover_size) - h
alpha = min(1.0, ((value+1)**2)-1)
r = 255
g = 0
b = 0
# Keep alpha at 255 (fully opaque)
color = pr.Color(r, g, b, int(255 * alpha))
# 4. Draw the bar
# Use max(1, w) to ensure high-frequency bars don't disappear on small screens
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)
size = pr.Vector2(current_height * 0.1, current_height * 0.1)
@@ -385,3 +427,5 @@ if texture is not None:
pr.unload_texture(texture)
pr.close_window()
close_event.set()
# save_state_thread.join()