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

+55 -1
View File
@@ -34,11 +34,13 @@ static QWidget *wrapBottomBtn(QPushButton *btn, int pad) {
return w;
}
InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *parent)
: QWidget(parent)
, m_input(new QTextEdit(this))
, m_emojiBtn(nullptr)
, m_pttBtn(nullptr)
, m_emojiPanel(emojiPanel)
, m_recorder(recorder)
{
auto *layout = new QHBoxLayout(this);
layout->setContentsMargins(8, 4, 8, 4);
@@ -71,6 +73,10 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
QString::fromUtf8("Смайлики"), this);
layout->addWidget(wrapBottomBtn(m_emojiBtn, 3), 0, Qt::AlignBottom);
m_pttBtn = makeIconBtn(QString::fromUtf8("\xF0\x9F\x8E\xA4"),
QString::fromUtf8("Голосовое сообщение (удерживайте)"), this);
layout->addWidget(wrapBottomBtn(m_pttBtn, 3), 0, Qt::AlignBottom);
auto *sendBtn = makeIconBtn(QString::fromUtf8("\xE2\x9E\xA4"),
QString::fromUtf8("Отправить"), this);
sendBtn->setStyleSheet(sendBtn->styleSheet() +
@@ -80,6 +86,10 @@ InputBar::InputBar(EmojiPanel *emojiPanel, QWidget *parent)
connect(m_input, &QTextEdit::textChanged, this, &InputBar::adjustInputHeight);
connect(sendBtn, &QPushButton::clicked, this, &InputBar::onSendClicked);
/* PTT button: press to record, release to stop */
connect(m_pttBtn, &QPushButton::pressed, this, &InputBar::onPttPressed);
connect(m_pttBtn, &QPushButton::released, this, &InputBar::onPttReleased);
if (m_emojiPanel) {
connect(m_emojiBtn, &QPushButton::clicked, this, [this]() {
m_emojiPanel->setVisible(!m_emojiPanel->isVisible());
@@ -140,3 +150,47 @@ void InputBar::onSendClicked() {
if (m_emojiPanel) m_emojiPanel->hide();
m_input->setFocus();
}
#include "audiorecorder.h"
#include "voicemessageencoder.h"
void InputBar::onPttPressed() {
if (!m_recorder || !m_recorder->isInitialized()) return;
m_recorder->startRecording();
m_pttActive = true;
m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() +
"QPushButton { background: #e74c3c; color: white; border-radius: 4px; }");
}
void InputBar::onPttReleased() {
if (!m_pttActive) return;
m_pttActive = false;
m_recorder->stopRecording();
m_pttBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 4px; font-size: 18px; background: transparent; }"
"QPushButton:hover { background: palette(light); }");
int durationMs = m_recorder->durationMs();
if (durationMs < 500) {
qDebug("InputBar: voice message too short (%dms), discarded", durationMs);
return;
}
const std::vector<int16_t>& pcm = m_recorder->buffer();
if (pcm.empty()) return;
QString mediaPath = VoiceEncoder::mediaDir(
QString::fromUtf8(qgetenv("CHAT_DATA_DIR").isEmpty() ? "chat_data" : qgetenv("CHAT_DATA_DIR")),
"current");
VoiceEncoder::ensureMediaDir(mediaPath);
QString fileName = VoiceEncoder::generateFileName();
QString filePath = mediaPath + "/" + fileName;
float durationSec = 0;
int frames = VoiceEncoder::encodeToFile(pcm, 48000, 1, filePath, durationSec);
if (frames > 0) {
m_voiceFilePath = filePath;
qDebug("InputBar: voice message ready: %s (%.1fs)", qPrintable(filePath), durationSec);
emit sendVoiceMessage(filePath, durationMs);
}
}