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:
1 parent
4a23593020
commit
4fde0b0325
260 files changed
+72665
-16
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user