Repository: sunnypilot/sunnypilot
Pull request #654: Device: Quiet Mode
Author: login: rav4kumar
Labels: name: enhancement
description: New feature or request
name: ui
description: UI change
name: dev-c3
description: This label will flag the PR used on it to be brought into dev-c3 branch
Created: 2025-03-10T05:21:31Z | Merged: 2025-03-24T03:15:11Z

## Summary by Sourcery

Introduces a quiet mode feature that allows users to disable certain audible alerts.

New Features:
- Adds a quiet mode setting to disable non-critical audible alerts.
- Adds a button in the device settings panel to toggle quiet mode on or off.

Tests:
- Updates UI tests to account for the new quiet mode button.


Diff:
diff --git a/common/params_keys.h b/common/params_keys.h
index 8237bdabb13..710a354a688 100644
--- a/common/params_keys.h
+++ b/common/params_keys.h
@@ -128,6 +128,7 @@ inline static std::unordered_map<std::string, uint32_t> keys = {
     {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION},
     {"OffroadMode", CLEAR_ON_MANAGER_START},
     {"OffroadMode_Status", CLEAR_ON_MANAGER_START},
+    {"QuietMode", PERSISTENT | BACKUP},
 
     // MADS params
     {"Mads", PERSISTENT | BACKUP},
diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py
index 8b5fc8bd04a..facb3983399 100644
--- a/selfdrive/ui/soundd.py
+++ b/selfdrive/ui/soundd.py
@@ -13,6 +13,8 @@
 
 from openpilot.system import micd
 
+from openpilot.selfdrive.ui.sunnypilot.quiet_mode import QuietMode
+
 SAMPLE_RATE = 48000
 SAMPLE_BUFFER = 4096 # (approx 100ms)
 MAX_VOLUME = 1.0
@@ -50,8 +52,10 @@ def check_selfdrive_timeout_alert(sm):
   return False
 
 
-class Soundd:
+class Soundd(QuietMode):
   def __init__(self):
+    super().__init__()
+
     self.load_sounds()
 
     self.current_alert = AudibleAlert.none
@@ -81,7 +85,7 @@ def get_sound_data(self, frames): # get "frames" worth of data from the current
 
     ret = np.zeros(frames, dtype=np.float32)
 
-    if self.current_alert != AudibleAlert.none:
+    if self.should_play_sound(self.current_alert):
       num_loops = sound_list[self.current_alert][1]
       sound_data = self.loaded_sounds[self.current_alert]
       written_frames = 0
@@ -144,6 +148,8 @@ def soundd_thread(self):
       while True:
         sm.update(0)
 
+        self.load_param()
+
         if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert
           self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb)
           self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x))
diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc
index eef025b0a5e..f26ed0a1985 100644
--- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc
+++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc
@@ -17,6 +17,7 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
   device_grid_layout->setVerticalSpacing(25);
 
   std::vector<std::tuple<QString, QString, QString>> device_btns = {
+    {"quietModeBtn", tr("Quiet Mode"), "QuietMode"},
     {"dcamBtn", tr("Driver Camera Preview"), ""},
     {"retrainingBtn", tr("Training Guide"), ""},
     {"regulatoryBtn", tr("Regulatory"), ""},
@@ -45,6 +46,8 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) {
 
   connect(buttons["dcamBtn"], &PushButtonSP::clicked, [=]() { emit showDriverView(); });
 
+  connect(buttons["quietModeBtn"], &PushButtonSP::clicked, buttons["quietModeBtn"], &PushButtonSP::updateButton);
+
   connect(buttons["retrainingBtn"], &PushButtonSP::clicked, [=]() {
     if (ConfirmationDialog::confirm(tr("Are you sure you want to review the training guide?"), tr("Review"), this)) {
       emit reviewTrainingGuide();
diff --git a/selfdrive/ui/sunnypilot/quiet_mode.py b/selfdrive/ui/sunnypilot/quiet_mode.py
new file mode 100644
index 00000000000..893df95033b
--- /dev/null
+++ b/selfdrive/ui/sunnypilot/quiet_mode.py
@@ -0,0 +1,39 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietMode:
+  def __init__(self):
+    self.params = Params()
+    self.enabled: bool = self.params.get_bool("QuietMode")
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame % 50 == 0:  # 2.5 seconds
+      self.enabled = self.params.get_bool("QuietMode")
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the Quiet Mode setting
+    and the current alert.
+    """
+    if not self.enabled:
+      return bool(current_alert != AudibleAlert.none)
+
+    return current_alert in ALERTS_ALWAYS_PLAY
diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py
index 1d7437bf651..c28b578d955 100755
--- a/selfdrive/ui/tests/test_ui/run.py
+++ b/selfdrive/ui/tests/test_ui/run.py
@@ -151,7 +151,7 @@ def setup_keyboard_uppercase(click, pm: PubMaster, scroll=None):
 
 def setup_driver_camera(click, pm: PubMaster, scroll=None):
   setup_settings_device(click, pm)
-  click(950, 620)
+  click(1720, 620)
   DATA['deviceState'].deviceState.started = False
   setup_onroad(click, pm)
   DATA['deviceState'].deviceState.started = True
diff --git a/system/manager/manager.py b/system/manager/manager.py
index 9266a62aaec..bcc2ff5c289 100755
--- a/system/manager/manager.py
+++ b/system/manager/manager.py
@@ -51,6 +51,7 @@ def manager_init() -> None:
     ("ModelManager_LastSyncTime", "0"),
     ("ModelManager_ModelsCache", ""),
     ("NeuralNetworkLateralControl", "0"),
+    ("QuietMode", "0"),
   ]
 
   if params.get_bool("RecordFrontLock"):


Reviews:
user: login: sunnyhaibin
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T23:43:34Z
user: login: sunnyhaibin
body: First pass.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T03:12:03Z
user: login: sunnyhaibin
body: Second pass. Love the type hints!
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T15:27:47Z
user: login: sunnyhaibin
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T18:18:42Z
user: login: sunnyhaibin
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-23T18:31:18Z
user: login: sunnyhaibin
body: LGTM. Thanks @rav4kumar  ❤️ 
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-24T03:10:42Z

Review comments:
diff_hunk: @@ -0,0 +1,39 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert = AudibleAlert.none
+    self.param_s = Params()
+    self.quiet_drive = self.param_s.get_bool("QuietDrive")
+    self._frame = 0
+
+  def load_param(self):
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = self.param_s.get_bool("QuietDrive")
+  def is_quiet_drive_enabled(self) -> bool:
+    return self.quiet_drive
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Static is failing, can you fix?
created_at: 2025-03-14T23:41:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert = AudibleAlert.none
+    self.param_s = Params()
+    self.quiet_drive = self.param_s.get_bool("QuietDrive")
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Add `bool` as type annotation here to pass mypy.
created_at: 2025-03-14T23:43:31Z
author_association: COLLABORATOR
diff_hunk: @@ -62,6 +64,14 @@ def __init__(self):
 
     self.spl_filter_weighted = FirstOrderFilter(0, 2.5, FILTER_DT, initialized=False)
 
+    self.quiet_drive_manager = QuietDriveManager()
+
+  def load_param(self):
+    self.quiet_drive_manager.load_param()
+
+  def should_play_sound(self):
+    return self.quiet_drive_manager.should_play_sound(self.current_alert)
+
path: selfdrive/ui/soundd.py
user: login: sunnyhaibin
body: Let's treat `QuietDriveManager` as a parent class of `Soundd` to better utilize inheritance and reduce boilerplate. The `load_param` and `should_play_sound` methods should be moved into `QuietDriveManager`, and called directly from `Soundd` if needed. This keeps `Soundd` clean and minimizes direct dependency injection.
created_at: 2025-03-23T02:56:55Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = bool(self.param_s.get_bool("QuietDrive"))
+
+  def is_quiet_drive_enabled(self) -> bool:
+    return bool(self.quiet_drive)
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the quiet drive setting
+    and the current alert.
+    """
+    AudibleAlert = car.CarControl.HUDControl.AudibleAlert
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: This is already imported at the top level. Remove.
created_at: 2025-03-23T03:00:17Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = bool(self.param_s.get_bool("QuietDrive"))
+
+  def is_quiet_drive_enabled(self) -> bool:
+    return bool(self.quiet_drive)
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the quiet drive setting
+    and the current alert.
+    """
+    AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+    # Define the conditions under which sounds should be played, using valid AudibleAlert values
+    return bool((self.current_alert == AudibleAlert.warningSoft or
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: No need to wrap boolean values with `bool()` if they're already boolean.
created_at: 2025-03-23T03:00:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = bool(self.param_s.get_bool("QuietDrive"))
+
+  def is_quiet_drive_enabled(self) -> bool:
+    return bool(self.quiet_drive)
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the quiet drive setting
+    and the current alert.
+    """
+    AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+    # Define the conditions under which sounds should be played, using valid AudibleAlert values
+    return bool((self.current_alert == AudibleAlert.warningSoft or
+                 self.current_alert == AudibleAlert.warningImmediate or
+                 self.current_alert == AudibleAlert.promptDistracted or
+                 self.current_alert == AudibleAlert.promptRepeat) or
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: You can use a `set` for `ALERTS_ALWAYS_PLAY` to simplify and speed up the check in `should_play_sound`, such as this:
```py
# Top level
ALERTS_ALWAYS_PLAY = {
  AudibleAlert.warningSoft,
  AudibleAlert.warningImmediate,
  AudibleAlert.promptDistracted,
  AudibleAlert.promptRepeat,
}

# Inside should_play_sound
should_play = current_alert in ALERTS_ALWAYS_PLAY or (not self.quiet_drive and current_alert != AudibleAlert.none)
```
created_at: 2025-03-23T03:03:02Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: ```suggestion
    if self._frame % 50 == 0:  # 2.5 seconds
```
created_at: 2025-03-23T03:08:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: ```suggestion
from cereal import car

from openpilot.common.params import Params
```
created_at: 2025-03-23T03:09:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: ```suggestion
    self.params = Params()
```
created_at: 2025-03-23T03:09:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: ```suggestion
    self.quiet_drive: bool = bool(self.params.get_bool("QuietDrive"))
```
created_at: 2025-03-23T03:09:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = bool(self.param_s.get_bool("QuietDrive"))
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: ```suggestion
      self.quiet_drive = bool(self.params.get_bool("QuietDrive"))
```
created_at: 2025-03-23T03:09:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,41 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from openpilot.common.params import Params
+from cereal import car
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = AudibleAlert.none  # Explicitly declare as int
+    self.param_s = Params()
+    self.quiet_drive: bool = bool(self.param_s.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame == 50:
+      self.quiet_drive = bool(self.param_s.get_bool("QuietDrive"))
+
+  def is_quiet_drive_enabled(self) -> bool:
+    return bool(self.quiet_drive)
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: If this is only used in `QuietDriveManager` only, we can just pass `self.quiet_drive` in `should_play_sound`.
created_at: 2025-03-23T03:11:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = car.CarControl.HUDControl.AudibleAlert.none
+    self.params = Params()
+    self.quiet_drive: bool = bool(self.params.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame % 50 == 0:  # 2.5 seconds
+      self.quiet_drive = bool(self.params.get_bool("QuietDrive"))
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the quiet drive setting
+    and the current alert.
+    """
+    ALERTS_ALWAYS_PLAY = {
+      car.CarControl.HUDControl.AudibleAlert.warningSoft,
+      car.CarControl.HUDControl.AudibleAlert.warningImmediate,
+      car.CarControl.HUDControl.AudibleAlert.promptDistracted,
+      car.CarControl.HUDControl.AudibleAlert.promptRepeat,
+    }
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: This is already declared at the top of the module. No need.
created_at: 2025-03-23T15:22:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = car.CarControl.HUDControl.AudibleAlert.none
+    self.params = Params()
+    self.quiet_drive: bool = bool(self.params.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame % 50 == 0:  # 2.5 seconds
+      self.quiet_drive = bool(self.params.get_bool("QuietDrive"))
+
+  def should_play_sound(self, current_alert: int) -> bool:
+    """
+    Check if a sound should be played based on the quiet drive setting
+    and the current alert.
+    """
+    ALERTS_ALWAYS_PLAY = {
+      car.CarControl.HUDControl.AudibleAlert.warningSoft,
+      car.CarControl.HUDControl.AudibleAlert.warningImmediate,
+      car.CarControl.HUDControl.AudibleAlert.promptDistracted,
+      car.CarControl.HUDControl.AudibleAlert.promptRepeat,
+    }
+    should_play = current_alert in ALERTS_ALWAYS_PLAY or (not self.quiet_drive and current_alert != car.CarControl.HUDControl.AudibleAlert.none)
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Use `AudibleAlert` instead of `car.CarControl.HUDControl.AudibleAlert`.
created_at: 2025-03-23T15:23:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = car.CarControl.HUDControl.AudibleAlert.none
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Do we really need to track this internally?
created_at: 2025-03-23T15:23:43Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = car.CarControl.HUDControl.AudibleAlert.none
+    self.params = Params()
+    self.quiet_drive: bool = bool(self.params.get_bool("QuietDrive"))
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Since `self.quiet_drive` is contained within this class, rename to `self.enabled` instead.
created_at: 2025-03-23T15:25:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,44 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.current_alert: int = car.CarControl.HUDControl.AudibleAlert.none
+    self.params = Params()
+    self.quiet_drive: bool = bool(self.params.get_bool("QuietDrive"))
+    self._frame = 0
+
+  def load_param(self) -> None:
+    self._frame += 1
+    if self._frame % 50 == 0:  # 2.5 seconds
+      self.quiet_drive = bool(self.params.get_bool("QuietDrive"))
+
+  def should_play_sound(self, current_alert: int) -> bool:
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: Try to end the loop early by using the stock condition, so we are not coupling with our custom logic later in the loop, such as:

```py
if not self.enabled:
  return xyz  # with stock condition checks here
```

Then, you can remove the stock conditions as part of the condition checks at the end of the loop.
created_at: 2025-03-23T15:27:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietDriveManager:
+  def __init__(self):
+    self.params = Params()
+    self.enabled: bool = bool(self.params.get_bool("QuietDrive"))
path: selfdrive/ui/sunnypilot/quietmode.py
user: login: sunnyhaibin
body: `get_bool` already returns as a bool.
```suggestion
    self.enabled: bool = self.params.get_bool("QuietDrive")
```
created_at: 2025-03-23T18:18:42Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+from cereal import car
+
+from openpilot.common.params import Params
+
+AudibleAlert = car.CarControl.HUDControl.AudibleAlert
+
+ALERTS_ALWAYS_PLAY = {
+  AudibleAlert.warningSoft,
+  AudibleAlert.warningImmediate,
+  AudibleAlert.promptDistracted,
+  AudibleAlert.promptRepeat,
+}
+
+class QuietMode:
+  def __init__(self):
+    self.params = Params()
+    self.enabled: bool = self.params.get_bool("QuietDrive")
+    self._frame = 0
+
+  def load_param(self) -> None:
path: selfdrive/ui/sunnypilot/quiet_mode.py
user: login: sunnyhaibin
body: This isn't used anywhere. If we need to update the toggle at runtime, it needs to be also in the loop of `Soundd`, preferably prior our custom logic check.
created_at: 2025-03-23T18:31:10Z
author_association: COLLABORATOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-03-10T05:21:35Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This pull request introduces a quiet mode feature that allows users to suppress most audible alerts, while still playing critical warnings. This is achieved by adding a new `QuietMode` class, a new parameter, and a button in the device settings panel. The `soundd` process is modified to use the `QuietMode` class to determine whether to play sounds.

#### Entity Relationship Diagram for QuietMode Parameter

```mermaid
erDiagram
    Params {
        string key
        string value
    }
    Params ||--o{ QuietMode : contains
    QuietMode {
        boolean enabled
    }
    note for Params "The Params entity stores system parameters."
    note for QuietMode "The QuietMode entity represents the state of the quiet mode setting."
```

#### Updated class diagram for Soundd and QuietMode

```mermaid
classDiagram
  class Soundd {
    -current_alert: AudibleAlert
    -loaded_sounds: dict
    -spl_filter_weighted: LowPassFilter
    -current_volume: float
    +__init__()
    +load_sounds()
    +get_sound_data(frames: int) np.ndarray
    +calculate_volume(spl: float) float
    +soundd_thread()
  }
  class QuietMode {
    -params: Params
    -enabled: bool
    -_frame: int
    +__init__()
    +load_param() None
    +should_play_sound(current_alert: int) bool
  }

  Soundd --|> QuietMode : inherits
  note for QuietMode "This class manages the quiet mode setting and determines whether a sound should be played based on the setting and the alert type."
  note for Soundd "This class handles the sound playback logic and integrates with the QuietMode class to respect the quiet mode setting."
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Implemented a quiet mode feature that suppresses most audible alerts, while still playing critical warnings. | <ul><li>Added a `QuietMode` class to manage the quiet mode setting and determine whether a sound should be played.</li><li>Added a new parameter `QuietMode` to control the feature.</li><li>Added a button in the device settings panel to toggle the quiet mode.</li><li>Modified `soundd` to inherit from `QuietMode` and use it to determine whether to play sounds.</li></ul> | `selfdrive/ui/soundd.py`<br/>`selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc`<br/>`selfdrive/ui/tests/test_ui/run.py`<br/>`common/params_keys.h`<br/>`system/manager/manager.py`<br/>`selfdrive/ui/sunnypilot/quiet_mode.py` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-10T05:25:41Z
body: <!-- _(run_id_screenshots **14026343696**)_ -->
## UI Preview
<details open><summary>settings_device : $${\color{red}\text{DIFFERENT}}$$</summary><table><tr>  <td> master <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_device_master_ref.png"> </td>  <td> proposed <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_device.png"> </td></tr><tr>  <td> diff <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_device_diff.png"> </td>  <td> composite diff <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_device_diff.gif"> </td></tr></table></details><details><summary>All Screenshots</summary><table><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/body.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/driver_camera.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/homescreen.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/keyboard.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/keyboard_uppercase.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/offroad_alert.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_alert_full.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_alert_mid.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_alert_small.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_disengaged.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_override.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_sidebar.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_wide.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/onroad_wide_sidebar.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/prime.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_developer.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_device.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_firehose.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_network.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_network_advanced.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_software.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_steering.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_steering_mads.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_sunnylink.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_sunnylink_sponsor_button.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_toggles.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_trips.png"> </td></tr><tr>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/settings_vehicle.png"> </td>  <td> <img src="https://raw.githubusercontent.com/sunnypilot/ci-artifacts/openpilot/pr-654/update_available.png"> </td></tr></table></details>
<!-- thollander/actions-comment-pull-request "run_id_screenshots" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-17T03:10:31Z
body: ⚠️ This PR was skipped in the automated `master-dev-c3-new` squash because Not all checks have passed.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-17T15:21:05Z
body: ## Squash and Merge

⚠️ This PR was skipped in the automated `master-dev-c3-new` squash because **not all checks have passed**.
author_association: CONTRIBUTOR