Browse Source

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
topo_upd
Evgeny 2 months ago
parent
commit
c2ad2e78c3
  1. 5426
      lib/dr_mp3.h
  2. 95864
      lib/miniaudio.h
  3. 4
      tools/chatgui/.clangd
  4. 5
      tools/chatgui/CMakeLists.txt
  5. 1
      tools/chatgui/resources/chatgui.qrc
  6. BIN
      tools/chatgui/resources/sounds/call_busy.mp3
  7. BIN
      tools/chatgui/resources/sounds/call_connect.mp3
  8. BIN
      tools/chatgui/resources/sounds/call_end.mp3
  9. BIN
      tools/chatgui/resources/sounds/call_incoming.mp3
  10. BIN
      tools/chatgui/resources/sounds/call_outgoing.mp3
  11. BIN
      tools/chatgui/resources/sounds/group_call_allowed.mp3
  12. BIN
      tools/chatgui/resources/sounds/group_call_connect.mp3
  13. BIN
      tools/chatgui/resources/sounds/group_call_end.mp3
  14. BIN
      tools/chatgui/resources/sounds/group_call_recording_start.mp3
  15. BIN
      tools/chatgui/resources/sounds/group_call_start.mp3
  16. BIN
      tools/chatgui/resources/sounds/msg_incoming.mp3
  17. 27
      tools/chatgui/src/mainwindow.cpp
  18. 8
      tools/chatgui/src/settingsdialog.cpp
  19. 2
      tools/chatgui/src/settingsdialog.h
  20. 124
      tools/chatgui/src/sound_manager.cpp
  21. 39
      tools/chatgui/src/sound_manager.h
  22. 120
      tools/chatgui/src/soundsettingspage.cpp
  23. 24
      tools/chatgui/src/soundsettingspage.h
  24. 2
      tools/chatgui/transport/miniaudio_impl.c
  25. 3
      tools/chatgui/transport/node_config.cpp

5426
lib/dr_mp3.h

File diff suppressed because it is too large Load Diff

95864
lib/miniaudio.h

File diff suppressed because it is too large Load Diff

4
tools/chatgui/.clangd

@ -0,0 +1,4 @@
If:
PathMatch: transport/miniaudio_impl\.c
Index:
Background: Skip

5
tools/chatgui/CMakeLists.txt

@ -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
tools/chatgui/resources/chatgui.qrc

@ -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>

BIN
tools/chatgui/resources/sounds/call_busy.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/call_connect.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/call_end.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/call_incoming.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/call_outgoing.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/group_call_allowed.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/group_call_connect.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/group_call_end.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/group_call_recording_start.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/group_call_start.mp3

Binary file not shown.

BIN
tools/chatgui/resources/sounds/msg_incoming.mp3

Binary file not shown.

27
tools/chatgui/src/mainwindow.cpp

@ -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
tools/chatgui/src/settingsdialog.cpp

@ -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
tools/chatgui/src/settingsdialog.h

@ -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
tools/chatgui/src/sound_manager.cpp

@ -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
tools/chatgui/src/sound_manager.h

@ -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
tools/chatgui/src/soundsettingspage.cpp

@ -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
tools/chatgui/src/soundsettingspage.h

@ -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
tools/chatgui/transport/miniaudio_impl.c

@ -0,0 +1,2 @@
#define MINIAUDIO_IMPLEMENTATION
#include "../../../lib/miniaudio.h"

3
tools/chatgui/transport/node_config.cpp

@ -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 = {

Loading…
Cancel
Save