mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-28 01:52:06 +08:00
BigUI WIP: Some Sounds
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user