BigUI WIP: Some Sounds

This commit is contained in:
firestarsdog
2026-04-14 03:21:25 -04:00
parent dc54284a3b
commit 936a50e35d
3 changed files with 231 additions and 137 deletions
@@ -521,6 +521,128 @@ class ValueTile(AetherTile):
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True)
class SliderTile(AetherTile):
def __init__(
self,
title: str,
get_value: Callable[[], float],
set_value: Callable[[float], None],
min_val: float,
max_val: float,
step: float,
icon_path: str | None = None,
bg_color: rl.Color | str | None = None,
is_enabled: Callable[[], bool] | None = None,
desc: str = "",
unit: str = "",
labels: dict[float, str] | None = None,
):
super().__init__(surface_color=bg_color)
self.title = title
self.desc = desc
self.get_value = get_value
self.set_value = set_value
self.min_val = min_val
self.max_val = max_val
self.step = step
self.unit = unit
self.labels = labels or {}
self.set_enabled(is_enabled or (lambda: True))
self._icon = starpilot_texture(icon_path, 80, 80) if icon_path else None
self._font = gui_app.font(FontWeight.BOLD)
self._font_desc = gui_app.font(FontWeight.NORMAL)
self._active_color = self.surface_color
self._disabled_color = rl.Color(120, 120, 120, 255)
self._is_dragging = False
self._last_mouse_x = 0.0
self._velocity = 0.0
self._smooth_value = get_value()
def _handle_mouse_press(self, mouse_pos: MousePos):
if rl.check_collision_point_rec(mouse_pos, self._hit_rect) and self.enabled:
self._is_pressed = True
self._is_dragging = True
self._last_mouse_x = mouse_pos.x
self._velocity = 0.0
def _handle_mouse_release(self, mouse_pos: MousePos):
if self._is_dragging:
self._is_dragging = False
self._is_pressed = False
def _handle_mouse_event(self, mouse_event):
if not rl.check_collision_point_rec(mouse_event.pos, self._hit_rect):
if not self._is_dragging:
self._plate_target = 0.0
if self._is_dragging:
dt = rl.get_frame_time()
current_val = self.get_value()
mouse_pos = mouse_event.pos
dx = mouse_pos.x - self._last_mouse_x
self._velocity = dx / max(dt, 0.001)
self._last_mouse_x = mouse_pos.x
rect_w = self._rect.width
if rect_w > 0:
val_range = self.max_val - self.min_val
val_dx = (dx / rect_w) * val_range
new_val = current_val + val_dx
abs_vel = abs(self._velocity)
snap_threshold = 800
coarse_step = 10 if val_range >= 100 else self.step * 5
dynamic_step = coarse_step if abs_vel > snap_threshold else self.step
snapped = round(new_val / dynamic_step) * dynamic_step
snapped = max(self.min_val, min(self.max_val, snapped))
if abs(snapped - current_val) >= self.step:
self.set_value(float(snapped))
def _render(self, rect: rl.Rectangle):
enabled = self.enabled
current_val = self.get_value()
dt = rl.get_frame_time()
self._smooth_value += (current_val - self._smooth_value) * (1 - math.exp(-dt / 0.1))
self.surface_color = self._active_color if enabled else self._disabled_color
if not enabled:
self._plate_offset = 0.0
self._plate_target = 0.0
face = self._render_layers(rect)
frac = (self._smooth_value - self.min_val) / (self.max_val - self.min_val)
fill_w = face.width * frac
if fill_w > 1:
fill_rect = rl.Rectangle(face.x, face.y, fill_w, face.height)
fill_color = rl.Color(
min(self.surface_color.r + 30, 255),
min(self.surface_color.g + 30, 255),
min(self.surface_color.b + 30, 255),
140
)
rl.draw_rectangle_rounded(fill_rect, TILE_RADIUS, 10, fill_color)
edge_x = face.x + fill_w
rl.draw_rectangle_rec(rl.Rectangle(edge_x - 3, face.y, 3, face.height), rl.Color(255, 255, 255, 80))
line_heights = [28, 28]
_, ty = self._centered_content(face, self._icon, 0.75, 28, len(line_heights), line_heights)
content_pad = SPACING.tile_content
max_w = face.width - (content_pad * 2)
self._draw_text_fit(self._font, self.title, rl.Vector2(face.x + content_pad, ty), max_w, 28, align_center=True, uppercase=True)
val_str = self.labels.get(current_val, f"{int(current_val)}{self.unit}")
self._draw_text_fit(self._font, val_str, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap), max_w, 28, align_center=True, uppercase=True)
if self.desc:
self._draw_text_fit(self._font_desc, self.desc, rl.Vector2(face.x + content_pad, ty + 28 + SPACING.line_gap + 34), max_w, 18, align_center=True)
class AetherSlider(Widget):
def __init__(
self,
@@ -8,7 +8,7 @@ import pyray as rl
from openpilot.common.params import Params
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SPACING
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SliderTile, SPACING
from openpilot.selfdrive.ui.layouts.settings.starpilot.sectioned_panel import SectionedTileLayout, TileSection
@@ -93,6 +93,22 @@ class StarPilotPanel(Widget):
if tile_type == "value":
return ValueTile(title=tr(cat["title"]), get_value=cat["get_value"], on_click=cat["on_click"], icon_path=cat.get("icon"), bg_color=cat.get("color"), is_enabled=cat.get("is_enabled"), desc=tr(cat.get("desc", "")))
if tile_type == "slider":
return SliderTile(
title=tr(cat["title"]),
get_value=cat["get_value"],
set_value=cat["set_value"],
min_val=cat["min_val"],
max_val=cat["max_val"],
step=cat["step"],
unit=cat.get("unit", ""),
labels=cat.get("labels", {}),
icon_path=cat.get("icon"),
bg_color=cat.get("color"),
is_enabled=cat.get("is_enabled"),
desc=tr(cat.get("desc", ""))
)
return None
def _build_tile_grid(self, categories: list[dict], columns: int | None = None, padding: int | None = None, uniform_width: bool = False) -> TileGrid:
+92 -136
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import subprocess
import time
from pathlib import Path
import pyray as rl
@@ -12,7 +13,8 @@ from openpilot.system.ui.widgets import DialogResult
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import StarPilotPanel
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, AetherSliderDialog, RadioTileGroup, SPACING
from openpilot.selfdrive.ui.layouts.settings.starpilot.tabbed_panel import TabSectionSpec, TabbedSectionHost
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, ToggleTile, SPACING
class StarPilotSoundsLayout(StarPilotPanel):
COOLDOWN_KEY = "SwitchbackModeCooldown"
@@ -37,79 +39,28 @@ class StarPilotSoundsLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._section_names = ["volume_control", "custom_alerts"]
self._active_section = self._section_names[0]
self._sub_panels = {
"volume_control": StarPilotVolumeControlLayout(),
"custom_alerts": StarPilotCustomAlertsLayout(),
}
self._section_tabs = RadioTileGroup(
"",
[tr("Volumes"), tr("Alerts")],
0,
self._on_section_change,
)
self._section_tabs = TabbedSectionHost([
TabSectionSpec("volume_control", "Volumes", StarPilotVolumeControlLayout()),
TabSectionSpec("custom_alerts", "Alerts", StarPilotCustomAlertsLayout()),
])
for name, panel in self._sub_panels.items():
if hasattr(panel, 'set_navigate_callback'):
panel.set_navigate_callback(lambda sub_panel, section_name=name: self._navigate_to_child(section_name, sub_panel))
if hasattr(panel, 'set_back_callback'):
panel.set_back_callback(self._go_back)
def set_navigate_callback(self, callback):
self._section_tabs.set_navigate_callback(callback)
def _on_section_change(self, index: int):
if 0 <= index < len(self._section_names):
previous_panel = self._sub_panels[self._active_section]
if hasattr(previous_panel, 'set_current_sub_panel'):
previous_panel.set_current_sub_panel("")
self._current_sub_panel = ""
self._set_active_section(self._section_names[index], "")
if self._navigate_callback:
self._navigate_callback("")
def _set_active_section(self, section_name: str, child_panel: str = ""):
if section_name not in self._sub_panels:
return
if section_name != self._active_section:
self._sub_panels[self._active_section].hide_event()
self._active_section = section_name
self._sub_panels[self._active_section].show_event()
self._section_tabs.set_index(self._section_names.index(section_name))
panel = self._sub_panels[section_name]
if hasattr(panel, 'set_current_sub_panel'):
panel.set_current_sub_panel(child_panel)
def _navigate_to_child(self, section_name: str, child_panel: str):
self._set_active_section(section_name, child_panel)
if self._navigate_callback:
self._navigate_callback(f"{section_name}:{child_panel}")
def set_current_sub_panel(self, sub_panel: str):
super().set_current_sub_panel(sub_panel)
if not sub_panel:
self._set_active_section(self._active_section, "")
return
if ":" in sub_panel:
section_name, child_panel = sub_panel.split(":", 1)
self._set_active_section(section_name, child_panel)
elif sub_panel in self._section_names:
self._set_active_section(sub_panel)
def set_back_callback(self, callback):
self._section_tabs.set_back_callback(callback)
def _render(self, rect):
tab_rect = rl.Rectangle(rect.x, rect.y, rect.width, SPACING.tab_height)
panel_rect = rl.Rectangle(rect.x, rect.y + SPACING.tab_height + SPACING.tab_panel_gap, rect.width, rect.height - SPACING.tab_height - SPACING.tab_panel_gap)
self._section_tabs.render(tab_rect)
self._sub_panels[self._active_section].render(panel_rect)
self._section_tabs.render(rect)
def set_current_sub_panel(self, sub_panel: str):
self._section_tabs.set_current_sub_panel(sub_panel)
def show_event(self):
super().show_event()
self._sub_panels[self._active_section].show_event()
self._section_tabs.show_event()
def hide_event(self):
super().hide_event()
self._sub_panels[self._active_section].hide_event()
self._section_tabs.hide_event()
class StarPilotVolumeControlLayout(StarPilotPanel):
COOLDOWN_INFO = {"title": tr_noop("Switchback Mode Cooldown"), "icon": "toggle_icons/icon_mute.png", "min": 0, "max": 30}
@@ -129,82 +80,82 @@ class StarPilotVolumeControlLayout(StarPilotPanel):
def __init__(self):
super().__init__()
self._init_sound_player()
self._tile_grid = TileGrid(columns=2, padding=SPACING.tile_gap, uniform_width=True)
self.CATEGORIES = []
for key in StarPilotSoundsLayout.VOLUME_KEYS:
info = self.VOLUME_INFO[key]
def get_val(k=key):
v = self._params.get_int(k, return_default=True, default=100)
if v == 0: return tr("Muted")
if v == 101: return tr("Auto")
return f"{v}%"
def on_click(k=key, i=info):
self._show_volume_selector(k, i)
self.CATEGORIES.append({
"title": info["title"],
"type": "value",
"get_value": get_val,
"on_click": on_click,
"icon": info["icon"],
"color": "#E63956"
})
def get_cooldown_val():
v = self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0)
if v == 0:
return tr("Off")
if v == 1:
return tr("1 minute")
return f"{v} {tr('minutes')}"
self.CATEGORIES.append({
"title": self.COOLDOWN_INFO["title"],
"type": "value",
"get_value": get_cooldown_val,
"on_click": self._show_cooldown_selector,
"icon": self.COOLDOWN_INFO["icon"],
"color": "#E63956"
})
self._pending_sound_test = None
self._last_sound_change_time = 0.0
self.SECTIONS = [
{
"title": tr_noop("Volume Levels"),
"categories": self._build_volume_categories(),
},
{
"title": tr_noop("Safety & Cooldown"),
"categories": self._build_safety_categories(),
}
]
self._rebuild_grid()
def _show_volume_selector(self, key: str, info: dict):
current_v = self._params.get_int(key, return_default=True, default=100)
def on_close(res, val):
if res == DialogResult.CONFIRM:
def _update_state(self):
super()._update_state()
if self._pending_sound_test and (time.monotonic() - self._last_sound_change_time) > 0.8:
self._test_sound(self._pending_sound_test)
self._pending_sound_test = None
def _build_volume_categories(self):
cats = []
for key in StarPilotSoundsLayout.VOLUME_KEYS:
info = self.VOLUME_INFO[key]
def get_val(k=key):
return float(self._params.get_int(k, return_default=True, default=100))
def set_val(val, k=key):
new_v = int(val)
if new_v != 101 and new_v < info["min"]:
new_v = info["min"]
self._params.put_int(key, new_v)
self._test_sound(key)
self._rebuild_grid()
if new_v != 101 and new_v < self.VOLUME_INFO[k]["min"]:
new_v = self.VOLUME_INFO[k]["min"]
self._params.put_int(k, new_v)
self._pending_sound_test = k
self._last_sound_change_time = time.monotonic()
gui_app.set_modal_overlay(AetherSliderDialog(
tr(info["title"]), 0, 101, 1, current_v, on_close,
unit="%", labels={0: tr("Muted"), 101: tr("Auto")}, color="#E63956"
))
cats.append({
"title": info["title"],
"type": "slider",
"get_value": get_val,
"set_value": set_val,
"min_val": 0,
"max_val": 101,
"step": 1,
"unit": "%",
"labels": {0: tr("Muted"), 101: tr("Auto")},
"icon": info["icon"],
"color": "#3B82F6"
})
return cats
def _show_cooldown_selector(self):
current_v = self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0)
def _build_safety_categories(self):
def get_cooldown_val():
return float(self._params.get_int(StarPilotSoundsLayout.COOLDOWN_KEY, return_default=True, default=0))
def on_close(res, val):
if res == DialogResult.CONFIRM:
self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val))
self._rebuild_grid()
def set_cooldown_val(val):
self._params.put_int(StarPilotSoundsLayout.COOLDOWN_KEY, int(val))
gui_app.set_modal_overlay(AetherSliderDialog(
tr(self.COOLDOWN_INFO["title"]), 0, self.COOLDOWN_INFO["max"], 1, current_v, on_close,
unit=" min", labels={0: tr("Off")}, color="#E63956"
))
return [{
"title": self.COOLDOWN_INFO["title"],
"type": "slider",
"get_value": get_cooldown_val,
"set_value": set_cooldown_val,
"min_val": 0,
"max_val": float(self.COOLDOWN_INFO["max"]),
"step": 1,
"unit": " " + tr("min"),
"labels": {0: tr("Off"), 1: tr("1 minute")},
"icon": self.COOLDOWN_INFO["icon"],
"color": "#3B82F6"
}]
@classmethod
def _init_sound_player(cls):
if cls._sound_player_process is not None: return
if cls._sound_player_process is not None and cls._sound_player_process.poll() is None: return
program = """
import numpy as np
import sounddevice as sd
@@ -215,11 +166,13 @@ while True:
line = sys.stdin.readline()
if not line: break
path, volume = line.strip().split('|')
sound_file = wave.open(path, 'rb')
audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
sd.play(audio * float(volume), sound_file.getframerate())
with wave.open(path, 'rb') as sound_file:
audio = np.frombuffer(sound_file.readframes(sound_file.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
sd.play(audio * float(volume), sound_file.getframerate())
sd.wait()
except: pass
except Exception:
sd._terminate()
sd._initialize()
"""
cls._sound_player_process = subprocess.Popen(["python3", "-u", "-c", program], stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -240,6 +193,9 @@ while True:
sound_path = theme_path if theme_path.exists() else stock_path
if not sound_path.exists(): return
volume = self._params.get_int(key, return_default=True, default=100) / 100.0
if self._sound_player_process.poll() is not None:
self._sound_player_process = None
self._init_sound_player()
try:
self._sound_player_process.stdin.write(f"{sound_path}|{volume}\n".encode())
self._sound_player_process.stdin.flush()
@@ -267,7 +223,7 @@ class StarPilotCustomAlertsLayout(StarPilotPanel):
"get_state": lambda k=key: self._params.get_bool(k),
"set_state": lambda s, k=key: self._params.put_bool(k, s),
"icon": info["icon"],
"color": "#E63956",
"color": "#3B82F6",
"key": key # Store for visibility check
})
self._rebuild_grid()