Files
2026-01-20 08:31:36 -07:00

223 lines
9.9 KiB
C++

#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<std::tuple<QString, QString, QString, QString>> soundsToggles {
{"AlertVolumeControl", tr("Alert Volume Controller"), tr("<b>Set how loud each type of openpilot alert is</b> to keep routine prompts from becoming distracting."), "../../frogpilot/assets/toggle_icons/icon_mute.png"},
{"DisengageVolume", tr("Disengage Volume"), tr("<b>Set the volume for alerts when openpilot disengages.</b><br><br>Examples include: \"Cruise Fault: Restart the Car\", \"Parking Brake Engaged\", \"Pedal Pressed\"."), ""},
{"EngageVolume", tr("Engage Volume"), tr("<b>Set the volume for the chime when openpilot engages</b>, such as after pressing the \"RESUME\" or \"SET\" steering wheel buttons."), ""},
{"PromptVolume", tr("Prompt Volume"), tr("<b>Set the volume for prompts that need attention.</b><br><br>Examples include: \"Car Detected in Blindspot\", \"Steering Temporarily Unavailable\", \"Turn Exceeds Steering Limit\"."), ""},
{"PromptDistractedVolume", tr("Prompt Distracted Volume"), tr("<b>Set the volume for prompts when openpilot detects driver distraction or unresponsiveness.</b><br><br>Examples include: \"Pay Attention\", \"Touch Steering Wheel\"."), ""},
{"RefuseVolume", tr("Refuse Volume"), tr("<b>Set the volume for alerts when openpilot refuses to engage.</b><br><br>Examples include: \"Brake Hold Active\", \"Door Open\", \"Seatbelt Unlatched\"."), ""},
{"WarningSoftVolume", tr("Warning Soft Volume"), tr("<b>Set the volume for softer warnings about potential risks.</b><br><br>Examples include: \"BRAKE! Risk of Collision\", \"Steering Temporarily Unavailable\"."), ""},
{"WarningImmediateVolume", tr("Warning Immediate Volume"), tr("<b>Set the volume for the loudest warnings that require urgent attention.</b><br><br>Examples include: \"DISENGAGE IMMEDIATELY — Driver Distracted\", \"DISENGAGE IMMEDIATELY — Driver Unresponsive\"."), ""},
{"CustomAlerts", tr("FrogPilot Alerts"), tr("<b>Optional FrogPilot alerts</b> that highlight driving events in a more noticeable way."), "../../frogpilot/assets/toggle_icons/icon_green_light.png"},
{"GoatScream", tr("Goat Scream"), tr("<b>Play the infamous \"Goat Scream\" when the steering controller reaches its limit.</b> Based on the \"Turn Exceeds Steering Limit\" event."), ""},
{"GreenLightAlert", tr("Green Light Alert"), tr("<b>Play an alert when the model predicts a red light has turned green.</b><br><br><i><b>Disclaimer</b>: 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.</i>"), ""},
{"LeadDepartingAlert", tr("Lead Departing Alert"), tr("<b>Play an alert when the lead vehicle departs from a stop.</b>"), ""},
{"LoudBlindspotAlert", tr("Loud \"Car Detected in Blindspot\" Alert"), tr("<b>Play a louder alert if a vehicle is in the blind spot when attempting to change lanes.</b> Based on the \"Car Detected in Blindspot\" event."), ""},
{"SpeedLimitChangedAlert", tr("Speed Limit Changed Alert"), tr("<b>Play an alert when the posted speed limit changes.</b>"), ""}
};
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<float, QString> volumeLabels;
for (int i = 0; i <= 101; ++i) {
volumeLabels[i] = i == 0 ? tr("Muted") : i == 101 ? tr("Auto") : QString::number(i) + "%";
}
std::vector<QString> 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<FrogPilotManageControl*>(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<FrogPilotParamValueButtonControl*>(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());
}
}