#include "starpilot/ui/qt/offroad/sounds_settings.h" StarPilotSoundsPanel::StarPilotSoundsPanel(StarPilotSettingsWindow *parent, bool forceOpen) : StarPilotListWidget(parent), parent(parent) { forceOpenDescriptions = forceOpen; QStackedLayout *soundsLayout = new QStackedLayout(); addItem(soundsLayout); StarPilotListWidget *soundsList = new StarPilotListWidget(this); ScrollView *soundsPanel = new ScrollView(soundsList, this); soundsLayout->addWidget(soundsPanel); StarPilotListWidget *alertVolumeControlList = new StarPilotListWidget(this); StarPilotListWidget *customAlertsList = new StarPilotListWidget(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."), "../../starpilot/assets/toggle_icons/icon_mute.png"}, {"SwitchbackModeCooldown", tr("Switchback Mode Cooldown"), tr("Set the minimum time between repeated steering-limit and minimum-steer-speed alerts while \"Switchback Mode\" is active.

Useful on winding roads where \"Turn Exceeds Steering Limit\" and \"Steer Unavailable Under\" can repeat frequently. Set to Off to disable the cooldown even when the mode is on."), ""}, {"BelowSteerSpeedVolume", tr("Min Steer Speed Alert Volume"), tr("Set the volume for the \"Steer Unavailable Under\" alert shown below the car's minimum steering speed.

Set to Muted to silence only this alert."), ""}, {"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("StarPilot Alerts"), tr("Optional StarPilot alerts that highlight driving events in a more noticeable way."), "../../starpilot/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."), ""}, {"GoatScreamCriticalAlerts", tr("Goat Scream Critical Alerts"), tr("Play the infamous \"Goat Scream\" for full-screen critical alerts that require immediate takeover.

Examples include: \"TAKE CONTROL IMMEDIATELY\" and \"Stock AEB: Risk of Collision\"."), ""}, {"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."), ""}, {"LoudBlindspotAlertWhenDisengaged", tr("Blind Spot Alert When Disengaged"), tr("Play the loud blind spot alert while lateral control is off or paused.

Useful when steering pauses on turn signal, since the lane-change state machine is inactive then."), ""}, {"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") { StarPilotManageControl *alertVolumeControlToggle = new StarPilotManageControl(param, title, desc, icon); QObject::connect(alertVolumeControlToggle, &StarPilotManageControl::manageButtonClicked, [soundsLayout, alertVolumeControlPanel]() { soundsLayout->setCurrentWidget(alertVolumeControlPanel); }); soundsToggle = alertVolumeControlToggle; } else if (alertCooldownKeys.contains(param)) { std::map cooldownLabels; for (int i = 0; i <= 30; ++i) { cooldownLabels[i] = i == 0 ? tr("Off") : i == 1 ? tr("1 minute") : QString::number(i) + tr(" minutes"); } soundsToggle = new StarPilotParamValueControl(param, title, desc, icon, 0, 30, QString(), cooldownLabels, 1); } 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 StarPilotParamValueButtonControl(param, title, desc, icon, 25, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); } else { soundsToggle = new StarPilotParamValueButtonControl(param, title, desc, icon, 0, 101, QString(), volumeLabels, 1, true, {}, alertButton, false, false); } } else if (param == "CustomAlerts") { StarPilotManageControl *customAlertsToggle = new StarPilotManageControl(param, title, desc, icon); QObject::connect(customAlertsToggle, &StarPilotManageControl::manageButtonClicked, [soundsLayout, customAlertsPanel]() { soundsLayout->setCurrentWidget(customAlertsPanel); }); soundsToggle = customAlertsToggle; } else { soundsToggle = new ParamControl(param, title, desc, icon); } toggles[param] = soundsToggle; if (alertCooldownKeys.contains(param) || alertVolumeControlKeys.contains(param)) { alertVolumeControlList->addItem(soundsToggle); } else if (customAlertsKeys.contains(param)) { customAlertsList->addItem(soundsToggle); } else { soundsList->addItem(soundsToggle); parentKeys.insert(param); } if (StarPilotManageControl *frogPilotManageToggle = qobject_cast(soundsToggle)) { QObject::connect(frogPilotManageToggle, &StarPilotManageControl::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) { StarPilotParamValueButtonControl *toggle = static_cast(toggles[key]); QObject::connect(toggle, &StarPilotParamValueButtonControl::buttonClicked, [key, toggle, this]() { toggle->updateParam(); testSound(key); }); } QObject::connect(parent, &StarPilotSettingsWindow::closeSubPanel, [soundsLayout, soundsPanel, this] { openDescriptions(forceOpenDescriptions, toggles); soundsLayout->setCurrentWidget(soundsPanel); }); QObject::connect(uiState(), &UIState::uiUpdate, this, &StarPilotSoundsPanel::updateState); for (auto &[key, toggle] : toggles) { if (alertCooldownKeys.contains(key) || alertVolumeControlKeys.contains(key)) { toggle->setVisible(true); } } initializeSoundPlayer(); updateToggles(); } void StarPilotSoundsPanel::showEvent(QShowEvent *event) { updateToggles(); } void StarPilotSoundsPanel::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 StarPilotSoundsPanel::updateState(const UIState &s) { if (!isVisible()) { return; } started = s.scene.started; } void StarPilotSoundsPanel::updateToggles() { const bool showAllToggles = parent->showAllTogglesEnabled(); for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { toggle->setVisible(showAllToggles); } } for (auto &[key, toggle] : toggles) { if (parentKeys.contains(key)) { continue; } bool setVisible = showAllToggles || parent->tuningLevel >= parent->starpilotToggleLevels[key].toDouble(); if (!showAllToggles) { if (key == "LoudBlindspotAlert") { setVisible &= parent->hasBSM; } else if (key == "LoudBlindspotAlertWhenDisengaged") { setVisible &= parent->hasBSM && params.getBool("LoudBlindspotAlert"); } else if (key == "SpeedLimitChangedAlert") { setVisible &= params.getBool("ShowSpeedLimits") || (parent->hasOpenpilotLongitudinal && params.getBool("SpeedLimitController")); } } toggle->setVisible(setVisible); if (setVisible) { if (alertCooldownKeys.contains(key) || alertVolumeControlKeys.contains(key)) { toggles["AlertVolumeControl"]->setVisible(true); } else if (customAlertsKeys.contains(key)) { toggles["CustomAlerts"]->setVisible(true); } } } openDescriptions(forceOpenDescriptions, toggles); update(); } void StarPilotSoundsPanel::testSound(const QString &key) { QString baseName = QString(key).remove("Volume"); if (started) { updateStarPilotToggles(); util::sleep_for(UI_FREQ); QString camelCaseAlert = baseName == "BelowSteerSpeed" ? "belowSteerSpeed" : QString(baseName).replace(0, 1, baseName[0].toLower()); params_memory.put("TestAlert", camelCaseAlert.toStdString()); } else { QString previewBaseName = baseName == "BelowSteerSpeed" ? "Prompt" : baseName; QString snakeCaseAlert = QString(previewBaseName).replace(QRegularExpression("([A-Z])"), "_\\1").toLower().mid(1); QString stockPath = "../../selfdrive/assets/sounds/" + snakeCaseAlert + ".wav"; QString themePath = "../../starpilot/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()); } }