chatgui: add sound playback via miniaudio (MP3 notification sounds)

- SoundManager singleton wraps ma_engine + ma_sound (non-blocking, background thread)
- MP3 files embedded in QRC, decoded to PCM at startup with MA_SOUND_FLAG_DECODE
- miniaudio.h (MIT) in lib/, IMPLEMENTATION in separate miniaudio_impl.c
- Sound settings page in SettingsDialog: enable/disable, volume slider (0-100%)
- Config keys [gui] sound_enabled, sound_volume
- Incoming messages trigger msg_incoming.mp3 sound
This commit is contained in:
Evgeny committed 2026-07-19 14:46:21 +03:00
1 parent f6f7e12192
commit c2ad2e78c3
25 files changed
+101647 -2

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
If:
PathMatch: transport/miniaudio_impl\.c
Index:
Background: Skip
+4 -1
View File
@@ -71,6 +71,8 @@ add_executable(chatgui
src/databasesettingspage.cpp
src/statuspage.cpp
src/nodespage.cpp
src/soundsettingspage.cpp
src/sound_manager.cpp
transport/utun_node.cpp
transport/node_config.cpp
transport/config_updater.cpp
@@ -79,6 +81,7 @@ add_executable(chatgui
transport/chat_sync.c
transport/member_sync.c
transport/merkle_sync.c
transport/miniaudio_impl.c
db/db_manager.cpp
../../lib/sqlite3.c
resources/chatgui.qrc
@@ -86,7 +89,7 @@ add_executable(chatgui
target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db)
target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1 USE_SQLITE)
set_source_files_properties(../../lib/sqlite3.c transport/chat_core.c transport/chat_sync.c transport/member_sync.c transport/merkle_sync.c PROPERTIES LANGUAGE C)
set_source_files_properties(../../lib/sqlite3.c transport/chat_core.c transport/chat_sync.c transport/member_sync.c transport/merkle_sync.c transport/miniaudio_impl.c PROPERTIES LANGUAGE C)
if(WIN32)
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread)
else()
+1
View File
@@ -5,5 +5,6 @@
<file>animations/greeting.tgs</file>
<file>animations/stop.tgs</file>
<file>animations/robot.tgs</file>
<file>sounds/msg_incoming.mp3</file>
</qresource>
</RCC>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+27
View File
@@ -7,6 +7,7 @@
#include "settingsdialog.h"
#include "creategroupdialog.h"
#include "invite_link.h"
#include "sound_manager.h"
#include "../db/db_manager.h"
#include "../transport/utun_node.h"
#include "../transport/gui_bridge.h"
@@ -18,6 +19,8 @@
#include <QMessageBox>
#include <QDebug>
#include <QSettings>
#include <QFile>
#include <QTextStream>
extern "C" {
#include "../transport/chat_core.h"
@@ -80,6 +83,29 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
setupUi();
setupTray();
SoundManager* sm = SoundManager::instance();
if (sm->init()) {
QFile cfgFile(m_cfgPath);
if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
bool inGui = false; bool enabled = true; int volume = 80;
QTextStream in(&cfgFile);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line == "[gui]") { inGui = true; continue; }
if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue;
if (line.startsWith("sound_enabled="))
enabled = line.mid(15).trimmed() != "0";
else if (line.startsWith("sound_volume="))
volume = line.mid(13).trimmed().toInt();
}
cfgFile.close();
sm->setEnabled(enabled);
sm->setVolume(volume / 100.0f);
}
sm->loadSound("msg_incoming", ":/sounds/msg_incoming.mp3");
}
if (m_db) {
s_mainWindow = this;
gui_bridge_set_msg_received_cb(onMsgReceivedCallback);
@@ -277,6 +303,7 @@ void MainWindow::showSettings() {
void MainWindow::onMessageReceived(const char* ch_id) {
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MainWindow: msg received ch=%s", ch_id);
SoundManager::instance()->play("msg_incoming");
if (m_messageList)
m_messageList->refresh();
}
+8
View File
@@ -4,6 +4,7 @@
#include "databasesettingspage.h"
#include "statuspage.h"
#include "nodespage.h"
#include "soundsettingspage.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
#include "../db/db_manager.h"
@@ -44,6 +45,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
m_categoryList->addItem("Database");
m_categoryList->addItem("Status");
m_categoryList->addItem("Nodes");
m_categoryList->addItem("Sound");
mainLayout->addWidget(m_categoryList);
// --- Right panel: stacked pages ---
@@ -83,6 +85,9 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
// Nodes page
m_nodesPage = new NodesPage(db, m_pages);
m_pages->addWidget(m_nodesPage);
m_soundPage = new SoundSettingsPage(m_pages);
m_pages->addWidget(m_soundPage);
rightLayout->addWidget(m_pages, 1);
// --- Bottom buttons ---
@@ -119,6 +124,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
// Load current values
loadProfileFromConfig(m_configPath);
m_networkPage->loadFromConfig(m_configPath);
m_soundPage->loadFromConfig(m_configPath);
m_categoryList->setCurrentRow(0);
}
@@ -142,6 +148,7 @@ void SettingsDialog::onCategoryChanged(int row) {
if (row == 2) m_databasePage->refreshStatus();
if (row == 3) m_statusPage->refreshStatus();
if (row == 4) m_nodesPage->refreshNodes();
if (row == 5) m_soundPage->loadFromConfig(m_configPath);
}
void SettingsDialog::onSave() {
@@ -181,5 +188,6 @@ void SettingsDialog::onSave() {
f.close();
m_networkPage->saveToConfig(m_configPath);
m_soundPage->saveToConfig(m_configPath);
accept();
}
+2
View File
@@ -12,6 +12,7 @@ class NetworkSettingsPage;
class DatabaseSettingsPage;
class StatusPage;
class NodesPage;
class SoundSettingsPage;
class DbManager;
class SettingsDialog : public QDialog {
@@ -36,4 +37,5 @@ private:
DatabaseSettingsPage* m_databasePage;
StatusPage* m_statusPage;
NodesPage* m_nodesPage;
SoundSettingsPage* m_soundPage;
};
+124
View File
@@ -0,0 +1,124 @@
#include "miniaudio.h"
#include "sound_manager.h"
#include <QFile>
#include <QDebug>
SoundManager* SoundManager::instance() {
static SoundManager s;
return &s;
}
SoundManager::~SoundManager() { shutdown(); }
bool SoundManager::init() {
if (m_initialized) return true;
ma_engine* engine = new ma_engine;
ma_result result = ma_engine_init(NULL, engine);
if (result != MA_SUCCESS) {
qWarning("SoundManager: ma_engine_init failed (%d)", result);
delete engine;
return false;
}
m_engine = engine;
m_initialized = true;
qDebug("SoundManager: initialized");
return true;
}
void SoundManager::shutdown() {
if (!m_initialized) return;
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
if (it->sound) {
ma_sound_uninit(it->sound);
delete it->sound;
}
}
m_sounds.clear();
if (m_engine) {
ma_engine_uninit(m_engine);
delete m_engine;
m_engine = nullptr;
}
m_initialized = false;
qDebug("SoundManager: shutdown");
}
void SoundManager::setEnabled(bool enabled) {
m_enabled = enabled;
qDebug("SoundManager: enabled=%d", enabled);
}
bool SoundManager::isEnabled() const { return m_enabled; }
void SoundManager::setVolume(float volume) {
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
m_volume = volume;
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
if (it->sound)
ma_sound_set_volume(it->sound, m_volume);
}
}
float SoundManager::volume() const { return m_volume; }
bool SoundManager::loadSound(const QString& name, const QString& qrcPath) {
if (!m_initialized) {
qWarning("SoundManager: not initialized, cannot load %s", qPrintable(name));
return false;
}
if (m_sounds.contains(name)) {
qWarning("SoundManager: sound '%s' already loaded", qPrintable(name));
return false;
}
QFile f(qrcPath);
if (!f.open(QIODevice::ReadOnly)) {
qWarning("SoundManager: cannot open %s", qPrintable(qrcPath));
return false;
}
QByteArray mp3Data = f.readAll();
f.close();
ma_decoder_config decConfig = ma_decoder_config_init_default();
ma_decoder decoder;
ma_result result = ma_decoder_init_memory(mp3Data.constData(), mp3Data.size(), &decConfig, &decoder);
if (result != MA_SUCCESS) {
qWarning("SoundManager: ma_decoder_init_memory failed for %s (%d)", qPrintable(name), result);
return false;
}
LoadedSound ls;
ls.sound = new ma_sound;
result = ma_sound_init_from_data_source(m_engine, (ma_data_source*)&decoder.ds,
MA_SOUND_FLAG_DECODE,
NULL, ls.sound);
ma_decoder_uninit(&decoder);
if (result != MA_SUCCESS) {
qWarning("SoundManager: sound init failed for %s (%d)", qPrintable(name), result);
delete ls.sound;
return false;
}
m_sounds.insert(name, ls);
qDebug("SoundManager: loaded '%s' from %s (%lld bytes)",
qPrintable(name), qPrintable(qrcPath), (long long)mp3Data.size());
return true;
}
void SoundManager::play(const QString& name) {
if (!m_enabled || !m_initialized) return;
auto it = m_sounds.find(name);
if (it == m_sounds.end()) return;
ma_sound* s = it->sound;
ma_sound_stop(s);
ma_sound_seek_to_pcm_frame(s, 0);
ma_sound_start(s);
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QHash>
#include <QString>
struct ma_engine;
struct ma_sound;
class SoundManager : public QObject {
Q_OBJECT
public:
static SoundManager* instance();
bool init();
void shutdown();
void setEnabled(bool enabled);
bool isEnabled() const;
void setVolume(float volume);
float volume() const;
bool loadSound(const QString& name, const QString& qrcPath);
void play(const QString& name);
private:
SoundManager() = default;
~SoundManager();
struct LoadedSound {
ma_sound* sound = nullptr;
};
ma_engine* m_engine = nullptr;
bool m_initialized = false;
bool m_enabled = true;
float m_volume = 0.8f;
QHash<QString, LoadedSound> m_sounds;
};
+120
View File
@@ -0,0 +1,120 @@
#include "soundsettingspage.h"
#include "sound_manager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QCheckBox>
#include <QSlider>
#include <QLabel>
#include <QFile>
#include <QTextStream>
#include <QDebug>
SoundSettingsPage::SoundSettingsPage(QWidget* parent)
: QWidget(parent)
{
auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 12, 0, 0);
layout->setSpacing(14);
m_enabledCheck = new QCheckBox("Enable sounds", this);
m_enabledCheck->setStyleSheet("font-size: 13px;");
layout->addWidget(m_enabledCheck);
auto* volLayout = new QHBoxLayout();
auto* volLabel = new QLabel("Volume:", this);
volLabel->setStyleSheet("font-size: 13px;");
volLayout->addWidget(volLabel);
m_volumeSlider = new QSlider(Qt::Horizontal, this);
m_volumeSlider->setRange(0, 100);
m_volumeSlider->setFixedWidth(200);
m_volumeSlider->setStyleSheet("QSlider::groove:horizontal { height: 6px; border-radius: 3px; background: palette(mid); }"
"QSlider::handle:horizontal { width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; background: #3390EC; }");
volLayout->addWidget(m_volumeSlider);
m_volumeLabel = new QLabel("80%", this);
m_volumeLabel->setFixedWidth(36);
m_volumeLabel->setStyleSheet("font-size: 13px;");
volLayout->addWidget(m_volumeLabel);
volLayout->addStretch();
layout->addLayout(volLayout);
layout->addStretch();
connect(m_enabledCheck, &QCheckBox::stateChanged, this, &SoundSettingsPage::onEnabledChanged);
connect(m_volumeSlider, &QSlider::valueChanged, this, [this](int val) {
m_volumeLabel->setText(QString::number(val) + "%");
});
}
void SoundSettingsPage::onEnabledChanged(int state) {
m_volumeSlider->setEnabled(state == Qt::Checked);
m_volumeLabel->setEnabled(state == Qt::Checked);
}
void SoundSettingsPage::loadFromConfig(const QString& path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
bool enabled = true;
int volume = 80;
bool inGui = false;
QTextStream in(&f);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line == "[gui]") { inGui = true; continue; }
if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue;
if (line.startsWith("sound_enabled="))
enabled = line.mid(QString("sound_enabled=").size()).trimmed() != "0";
else if (line.startsWith("sound_volume="))
volume = line.mid(QString("sound_volume=").size()).trimmed().toInt();
}
f.close();
m_enabledCheck->setChecked(enabled);
m_volumeSlider->setValue(volume);
m_volumeSlider->setEnabled(enabled);
m_volumeLabel->setEnabled(enabled);
m_volumeLabel->setText(QString::number(volume) + "%");
}
void SoundSettingsPage::saveToConfig(const QString& path) const {
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QString content = f.readAll();
f.close();
QStringList lines = content.split('\n');
bool inGui = false;
bool foundEnabled = false, foundVolume = false;
for (int i = 0; i < lines.size(); i++) {
QString line = lines[i].trimmed();
if (line == "[gui]") { inGui = true; continue; }
if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue;
if (line.startsWith("sound_enabled=")) {
lines[i] = QString("sound_enabled=%1").arg(m_enabledCheck->isChecked() ? 1 : 0);
foundEnabled = true;
} else if (line.startsWith("sound_volume=")) {
lines[i] = QString("sound_volume=%1").arg(m_volumeSlider->value());
foundVolume = true;
}
}
if (!foundEnabled || !foundVolume) {
for (int i = 0; i < lines.size(); i++) {
if (lines[i].trimmed() == "[gui]") {
if (!foundEnabled) lines.insert(i + 1, QString("sound_enabled=%1").arg(m_enabledCheck->isChecked() ? 1 : 0));
if (!foundVolume) lines.insert(i + 1, QString("sound_volume=%1").arg(m_volumeSlider->value()));
break;
}
}
}
f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
f.write(lines.join('\n').toUtf8());
f.close();
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <QWidget>
class QCheckBox;
class QSlider;
class QLabel;
class SoundSettingsPage : public QWidget {
Q_OBJECT
public:
explicit SoundSettingsPage(QWidget* parent = nullptr);
void loadFromConfig(const QString& configPath);
void saveToConfig(const QString& configPath) const;
private slots:
void onEnabledChanged(int state);
private:
QCheckBox* m_enabledCheck;
QSlider* m_volumeSlider;
QLabel* m_volumeLabel;
};
+2
View File
@@ -0,0 +1,2 @@
#define MINIAUDIO_IMPLEMENTATION
#include "../../../lib/miniaudio.h"
+2 -1
View File
@@ -36,7 +36,8 @@ static const QSet<QString> CONTROL_KEYS = {
};
static const QSet<QString> GUI_KEYS = {
"db_path", "debug_file", "debug_level", "debug_categories"
"db_path", "debug_file", "debug_level", "debug_categories",
"sound_enabled", "sound_volume"
};
static const QSet<QString> ALLOWED_KEYS_KEYS = {