#include "frogpilot/ui/qt/offroad/sounds_settings.h" FrogPilotSoundsPanel::FrogPilotSoundsPanel(FrogPilotSettingsWindow *parent, bool forceOpen) : FrogPilotListWidget(parent), parent(parent) { forceOpenDescriptions = forceOpen; QStackedLayout *soundsLayout = new QStackedLayout(); addItem(soundsLayout); FrogPilotListWidget *soundsList = new FrogPilotListWidget(this); ScrollView *soundsPanel = new ScrollView(soundsList, this); soundsLayout->addWidget(soundsPanel); FrogPilotListWidget *alertVolumeControlList = new FrogPilotListWidget(this); FrogPilotListWidget *customAlertsList = new FrogPilotListWidget(this); ScrollView *alertVolumeControlPanel = new ScrollView(alertVolumeControlList, this); ScrollView *customAlertsPanel = new ScrollView(customAlertsList, this); soundsLayout->addWidget(alertVolumeControlPanel); soundsLayout->addWidget(customAlertsPanel); const std::vector> soundsToggles { {"AlertVolumeControl", tr("Alert Volume Controller"), tr("Set how loud each type of openpilot alert is to keep routine prompts from becoming distracting."), "../../frogpilot/assets/toggle_icons/icon_mute.png"}, {"DisengageVolume", tr("Disengage Volume"), tr("Set the volume for alerts when openpilot disengages.

Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\"."), ""}, {"EngageVolume", tr("Engage Volume"), tr("Set the volume for the chime when openpilot engages, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons."), ""}, {"PromptVolume", tr("Prompt Volume"), tr("Set the volume for prompts that need attention.

Examples include: \"Car Detected in Blindspot\", \"Steering Temporarily Unavailable\", \"Turn Exceeds Steering Limit\"."), ""}, {"PromptDistractedVolume", tr("Prompt Distracted Volume"), tr("Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.

Examples include: \"Pay Attention\", \"Touch Steering Wheel\"."), ""}, {"RefuseVolume", tr("Refuse Volume"), tr("Set the volume for alerts when openpilot refuses to engage.

Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\"."), ""}, {"WarningSoftVolume", tr("Warning Soft Volume"), tr("Set the volume for softer warnings about potential risks.

Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\"."), ""}, {"WarningImmediateVolume", tr("Warning Immediate Volume"), tr("Set the volume for the loudest warnings that require urgent attention.

Examples include: \"DISENGAGE IMMEDIATELY — Driver Distracted\", \"DISENGAGE IMMEDIATELY — Driver Unresponsive\"."), ""}, {"CustomAlerts", tr("FrogPilot Alerts"), tr("Optional FrogPilot alerts that highlight driving events in a more noticeable way."), "../../frogpilot/assets/toggle_icons/icon_green_light.png"}, {"GoatScream", tr("Goat Scream"), tr("Play the infamous \"Goat Scream\" when the steering controller reaches its limit. Based on the \"Turn Exceeds Steering Limit\" event."), ""}, {"GreenLightAlert", tr("Green Light Alert"), tr("Play an alert when the model predicts a red light has turned green.

Disclaimer: openpilot does not explicitly detect traffic lights. This alert is based on end-to-end model predictions from camera input and may trigger even when the light has not changed."), ""}, {"LeadDepartingAlert", tr("Lead Departing Alert"), tr("Play an alert when the lead vehicle departs from a stop."), ""}, {"LoudBlindspotAlert", tr("Loud \"Car Detected in Blindspot\" Alert"), tr("Play a louder alert if a vehicle is in the blind spot when attempting to change lanes. Based on the \"Car Detected in Blindspot\" event."), ""}, {"SpeedLimitChangedAlert", tr("Speed Limit Changed Alert"), tr("Play an alert when the posted speed limit changes."), ""} }; for (const auto &[param, title, desc, icon] : soundsToggles) { AbstractControl *soundsToggle; if (param == "AlertVolumeControl") { FrogPilotManageControl *alertVolumeControlToggle = new FrogPilotManageControl(param, title, desc, icon); QObject::connect(alertVolumeControlToggle, &FrogPilotManageControl::manageButtonClicked, [soundsLayout, alertVolumeControlPanel]() { soundsLayout->setCurrentWidget(alertVolumeControlPanel); }); soundsToggle = alertVolumeControlToggle; } else if (alertVolumeControlKeys.contains(param)) { std::map volumeLabels; for (int i = 0; i <= 101; ++i) { volumeLabels[i] = i == 0 ? tr("Muted") : i == 101 ? tr("Auto") : QString::number(i) + "%"; } std::vector alertButton{tr("Test")}; if (param == "WarningImmediateVolume" || param == "WarningSoftVolume") { soundsToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 25, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); } else { soundsToggle = new FrogPilotParamValueButtonControl(param, title, desc, icon, 0, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); } } else if (param == "CustomAlerts") { FrogPilotManageControl *customAlertsToggle = new FrogPilotManageControl(param, title, desc, icon); QObject::connect(customAlertsToggle, &FrogPilotManageControl::manageButtonClicked, [soundsLayout, customAlertsPanel]() { soundsLayout->setCurrentWidget(customAlertsPanel); }); soundsToggle = customAlertsToggle; } else { soundsToggle = new ParamControl(param, title, desc, icon); } toggles[param] = soundsToggle; if (alertVolumeControlKeys.contains(param)) { alertVolumeControlList->addItem(soundsToggle); } else if (customAlertsKeys.contains(param)) { customAlertsList->addItem(soundsToggle); } else { soundsList->addItem(soundsToggle); parentKeys.insert(param); } if (FrogPilotManageControl *frogPilotManageToggle = qobject_cast(soundsToggle)) { QObject::connect(frogPilotManageToggle, &FrogPilotManageControl::manageButtonClicked, [this]() { emit openSubPanel(); openDescriptions(forceOpenDescriptions, toggles); }); } QObject::connect(soundsToggle, &AbstractControl::hideDescriptionEvent, [this]() { update(); }); QObject::connect(soundsToggle, &AbstractControl::showDescriptionEvent, [this]() { update(); }); } 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); }); QObject::connect(uiState(), &UIState::uiUpdate, this, &FrogPilotSoundsPanel::updateState); for (auto &[key, toggle] : toggles) { if (alertVolumeControlKeys.contains(key)) { toggle->setVisible(true); } } initializeSoundPlayer(); updateToggles(); } 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; } started = s.scene.started; } void FrogPilotSoundsPanel::updateToggles() { for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { toggle->setVisible(false); } } for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { continue; } bool setVisible = parent->tuningLevel >= parent->frogpilotToggleLevels[key].toDouble(); if (key == "LoudBlindspotAlert") { setVisible &= parent->hasBSM; } else if (key == "SpeedLimitChangedAlert") { setVisible &= params.getBool("ShowSpeedLimits") || (parent->hasOpenpilotLongitudinal && params.getBool("SpeedLimitController")); } toggle->setVisible(setVisible); if (setVisible) { if (alertVolumeControlKeys.contains(key)) { toggles["AlertVolumeControl"]->setVisible(true); } else if (customAlertsKeys.contains(key)) { toggles["CustomAlerts"]->setVisible(true); } } } openDescriptions(forceOpenDescriptions, toggles); 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()); } }