From 4b5c59d32cfe6e9bb0ab1c5f91dfc5d0cc8fd1ad Mon Sep 17 00:00:00 2001 From: James <91348155+FrogAi@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:00:00 -0700 Subject: [PATCH] Alert Volume Controller --- frogpilot/ui/qt/offroad/sounds_settings.cc | 58 ++++++++++++++++++++++ frogpilot/ui/qt/offroad/sounds_settings.h | 4 ++ selfdrive/ui/soundd.py | 35 ++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/frogpilot/ui/qt/offroad/sounds_settings.cc b/frogpilot/ui/qt/offroad/sounds_settings.cc index 703983932..7b5e8b636 100644 --- a/frogpilot/ui/qt/offroad/sounds_settings.cc +++ b/frogpilot/ui/qt/offroad/sounds_settings.cc @@ -98,6 +98,14 @@ FrogPilotSoundsPanel::FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent, bool }); } + for (const QString &key : alertVolumeControlKeys) { + FrogPilotParamValueButtonControl *toggle = static_cast(toggles[key]); + QObject::connect(toggle, &FrogPilotParamValueButtonControl::buttonClicked, [key, toggle, this]() { + toggle->updateParam(); + testSound(key); + }); + } + QObject::connect(parent, &FrogPilotSettingsWindow::closeSubPanel, [soundsLayout, soundsPanel, this] { openDescriptions(forceOpenDescriptions, toggles); soundsLayout->setCurrentWidget(soundsPanel); @@ -110,6 +118,8 @@ FrogPilotSoundsPanel::FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent, bool } } + initializeSoundPlayer(); + updateToggles(); } @@ -117,6 +127,33 @@ void FrogPilotSoundsPanel::showEvent(QShowEvent *event) { updateToggles(); } +void FrogPilotSoundsPanel::initializeSoundPlayer() { + QString program = R"( +import numpy as np +import sounddevice as sd +import sys +import wave + +while True: + try: + 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()) + sd.wait() + except Exception: + pass +)"; + + soundPlayerProcess = new QProcess(this); + soundPlayerProcess->start("python3", QStringList{"-u", "-c", program}); +} + void FrogPilotSoundsPanel::updateState(const UIState &s) { if (!isVisible()) { return; @@ -162,3 +199,24 @@ void FrogPilotSoundsPanel::updateToggles() { update(); } + +void FrogPilotSoundsPanel::testSound(const QString &key) { + QString baseName = QString(key).remove("Volume"); + + if (started) { + updateFrogPilotToggles(); + + util::sleep_for(UI_FREQ); + + QString camelCaseAlert = QString(baseName).replace(0, 1, baseName[0].toLower()); + params_memory.put("TestAlert", camelCaseAlert.toStdString()); + } else { + QString snakeCaseAlert = QString(baseName).replace(QRegularExpression("([A-Z])"), "_\\1").toLower().mid(1); + QString stockPath = "../../selfdrive/assets/sounds/" + snakeCaseAlert + ".wav"; + QString themePath = "../../frogpilot/assets/active_theme/sounds/" + snakeCaseAlert + ".wav"; + + float volume = params.getFloat(key.toStdString()) / 100.0f; + + soundPlayerProcess->write(((QFile::exists(themePath) ? themePath : stockPath) + "|" + QString::number(volume) + "\n").toUtf8()); + } +} diff --git a/frogpilot/ui/qt/offroad/sounds_settings.h b/frogpilot/ui/qt/offroad/sounds_settings.h index ce048de30..ada6a3ffa 100644 --- a/frogpilot/ui/qt/offroad/sounds_settings.h +++ b/frogpilot/ui/qt/offroad/sounds_settings.h @@ -15,6 +15,8 @@ protected: void showEvent(QShowEvent *event) override; private: + void initializeSoundPlayer(); + void testSound(const QString &key); void updateState(const UIState &s); void updateToggles(); @@ -32,4 +34,6 @@ private: Params params; Params params_memory{"", true}; + + QProcess *soundPlayerProcess; }; diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index b5779927a..779adc280 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -85,6 +85,8 @@ class Soundd: self.frogpilot_toggles = get_frogpilot_toggles() + self.auto_volume = 0 + self.update_frogpilot_sounds() def load_sounds(self): @@ -135,7 +137,10 @@ class Soundd: self.current_sound_frame = 0 def get_audible_alert(self, sm): - if sm.updated['selfdriveState']: + if self.params_memory.get("TestAlert"): + self.update_alert(getattr(AudibleAlert, self.params_memory.get("TestAlert"))) + self.params_memory.remove("TestAlert") + elif sm.updated['selfdriveState']: new_alert = sm['selfdriveState'].alertSound.raw # FrogPilot variables @@ -182,6 +187,15 @@ class Soundd: self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) + if self.frogpilot_toggles.alert_volume_controller: + self.auto_volume = self.current_volume + self.current_volume = 0.0 + + elif self.current_alert in self.volume_map and self.frogpilot_toggles.alert_volume_controller: + self.current_volume = self.volume_map[self.current_alert] + if self.current_volume == 1.01: + self.current_volume = self.auto_volume + self.get_audible_alert(sm) rk.keep_time() @@ -196,6 +210,25 @@ class Soundd: self.update_frogpilot_sounds() def update_frogpilot_sounds(self): + self.volume_map = { + AudibleAlert.engage: self.frogpilot_toggles.engage_volume / 100.0, + AudibleAlert.disengage: self.frogpilot_toggles.disengage_volume / 100.0, + AudibleAlert.refuse: self.frogpilot_toggles.refuse_volume / 100.0, + + AudibleAlert.prompt: self.frogpilot_toggles.prompt_volume / 100.0, + AudibleAlert.promptRepeat: self.frogpilot_toggles.prompt_volume / 100.0, + AudibleAlert.promptDistracted: self.frogpilot_toggles.promptDistracted_volume / 100.0, + + AudibleAlert.warningSoft: self.frogpilot_toggles.warningSoft_volume / 100.0, + AudibleAlert.warningImmediate: self.frogpilot_toggles.warningImmediate_volume / 100.0, + + FrogPilotAudibleAlert.goat: self.frogpilot_toggles.prompt_volume / 100.0, + FrogPilotAudibleAlert.startup: self.frogpilot_toggles.engage_volume / 100.0 + } + + for sound in sound_list: + if sound not in self.volume_map: + self.volume_map[sound] = 1.01 def main():