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:
1 parent
f6f7e12192
commit
c2ad2e78c3
25 files changed
+101647
-2
No files matched your search
@@ -0,0 +1,4 @@
|
||||
If:
|
||||
PathMatch: transport/miniaudio_impl\.c
|
||||
Index:
|
||||
Background: Skip
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "../../../lib/miniaudio.h"
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user