feat: integrate Opus codec into lib/libopus + voice messages in chatgui

- Add lib/libopus/ (opus v1.6.1 embedded source: CELT 18 .c, SILK 77 .c, SILK float 28 .c, OPUS core 14 .c)
- Add lib/opus_codec.h/.c (wrapper: encoder, decoder, raw PCM int16)
- Add tests/test_opus_codec.c (7 tests: silence, sine, compression, PLC, null, defaults, multi-frame)
- chatgui: AudioRecorder (miniaudio capture, 48kHz mono)
- chatgui: VoiceEncoder (PCM→Opus file with custom container)
- chatgui: VoicePlayer (Opus→PCM playback via SoundManager)
- chatgui: PTT button in InputBar, voice message send/receive in MessageList
- chatgui: AudioDeviceSettingsPage (device selection, persisted in ui_state)
- chatgui: MessageDelegate voice message rendering (play button + duration)
- Update autotools: configure.ac, Makefile.am, src/Makefile.am, tests/Makefile.am
- Update CMake: chatgui CMakeLists.txt, libutun CMakeLists.txt (link libopus_internal.a)
This commit is contained in:
Evgeny committed 2026-07-26 20:34:38 +03:00
1 parent 4a23593020
commit 4fde0b0325
260 files changed
+72665 -16

No files matched your search

+53 -2
View File
@@ -16,6 +16,7 @@
#include <QScrollBar>
#include <QApplication>
#include <QDebug>
#include <QFileInfo>
#include <vector>
#include <memory>
@@ -76,7 +77,7 @@ static void setMsg(QStandardItem *item,
item->setData(reactions, MsgReactionsRole);
}
MessageList::MessageList(DbManager* db, QWidget *parent)
MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent)
: QWidget(parent)
, m_view(new ChatView(this))
, m_model(new QStandardItemModel(this))
@@ -119,7 +120,7 @@ MessageList::MessageList(DbManager* db, QWidget *parent)
m_emojiPanel = new EmojiPanel(this);
layout->addWidget(m_emojiPanel);
m_inputBar = new InputBar(m_emojiPanel, this);
m_inputBar = new InputBar(m_emojiPanel, recorder, this);
layout->addWidget(m_inputBar);
connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) {
@@ -144,6 +145,33 @@ MessageList::MessageList(DbManager* db, QWidget *parent)
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString &filePath, int durationMs) {
if (m_currentChannelId.isEmpty()) return;
QFileInfo fi(filePath);
QString fileName = fi.fileName();
QString jsonData = QString("{\"n\":%1,\"ch\":\"%2\",\"ct\":\"audio/opus\",\"d\":\"%3\",\"dur\":%4}")
.arg(m_db ? m_db->myNodeId() : 0)
.arg(m_currentChannelId)
.arg(fileName)
.arg(durationMs / 1000.0, 0, 'f', 1);
QByteArray textData = jsonData.toUtf8();
auto ts = QDateTime::currentMSecsSinceEpoch();
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)textData.size();
memcpy(req->data, textData.constData(), textData.size());
req->timestamp = (uint64_t)ts;
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() {
m_view->viewport()->update();
});
@@ -224,6 +252,18 @@ void MessageList::loadChannel(const QString& channelId) {
item->setData(m.id, MsgIdRole);
item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole);
item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole);
item->setData(m.contentType, MsgContentTypeRole);
if (m.contentType == "audio/opus") {
/* JSON data contains {"dur":X.X} — parse duration */
QByteArray d = m.data;
int durIdx = d.indexOf("\"dur\":");
if (durIdx >= 0) {
int durEnd = d.indexOf(',', durIdx);
if (durEnd < 0) durEnd = d.indexOf('}', durIdx);
QByteArray durStr = d.mid(durIdx + 6, durEnd - durIdx - 6);
item->setData(QString::fromUtf8(durStr) + "s", MsgVoiceDurationRole);
}
}
m_model->appendRow(item);
}
@@ -287,6 +327,17 @@ void MessageList::refresh() {
item->setData(m.id, MsgIdRole);
item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole);
item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole);
item->setData(m.contentType, MsgContentTypeRole);
if (m.contentType == "audio/opus") {
QByteArray d = m.data;
int durIdx = d.indexOf("\"dur\":");
if (durIdx >= 0) {
int durEnd = d.indexOf(',', durIdx);
if (durEnd < 0) durEnd = d.indexOf('}', durIdx);
QByteArray durStr = d.mid(durIdx + 6, durEnd - durIdx - 6);
item->setData(QString::fromUtf8(durStr) + "s", MsgVoiceDurationRole);
}
}
m_model->appendRow(item);
while (m_model->rowCount() > 100)
m_model->removeRow(0);