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

+66
View File
@@ -67,6 +67,35 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
QString text = index.data(MsgTextRole).toString();
L.isOutgoing = index.data(MsgIsOutgoingRole).toBool();
/* Voice message detection */
QString contentType = index.data(MsgContentTypeRole).toString();
L.isVoice = (contentType == "audio/opus");
if (L.isVoice) {
L.voiceDuration = index.data(MsgVoiceDurationRole).toString();
QFontMetrics fm(option.font);
int lineH = fm.lineSpacing();
L.totalHeight = qMax(lineH * 2 + kGap * 2, kAvatar) + kItemGap;
L.bubbleWidth = 160;
int contentEdge = L.isOutgoing
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
if (!L.isOutgoing)
L.bubbleRect = QRect(contentEdge, 0, L.bubbleWidth, L.totalHeight - kItemGap);
else
L.bubbleRect = QRect(contentEdge - L.bubbleWidth, 0, L.bubbleWidth, L.totalHeight - kItemGap);
L.voicePlayBtnRect = QRect(L.bubbleRect.left() + kPadH, L.bubbleRect.top() + kPadTop + 4, 28, 28);
L.voiceDurationRect = QRect(L.bubbleRect.left() + kPadH + 36, L.bubbleRect.top() + kPadTop + 4,
L.bubbleWidth - kPadH * 2 - 36, 28);
if (!L.narrow) {
int avatarY = L.bubbleRect.bottom() - kAvatar;
if (!L.isOutgoing)
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
else
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar);
}
return L;
}
L.emojiCount = detectEmojiOnly(text);
L.emojiOnly = (L.emojiCount >= 1);
@@ -449,6 +478,43 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
return;
}
if (L.isVoice) {
drawBubble(painter, L, option);
if (!L.narrow) drawAvatar(painter, L, index);
/* Play button */
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0x33, 0x90, 0xEC));
QPainterPath playPath;
QRectF playR = L.voicePlayBtnRect;
qreal cx = playR.center().x() - playR.width() / 6.0;
qreal cy = playR.center().y();
qreal hs = playR.height() / 2.5;
playPath.moveTo(cx, cy - hs);
playPath.lineTo(cx + hs * 1.5, cy);
playPath.lineTo(cx, cy + hs);
playPath.closeSubpath();
painter->drawPath(playPath);
/* Duration text */
painter->setPen(option.palette.text().color());
painter->setFont(option.font);
if (!L.voiceDuration.isEmpty())
painter->drawText(L.voiceDurationRect, Qt::AlignLeft | Qt::AlignVCenter, L.voiceDuration);
/* Time */
QFont timeFont = option.font;
timeFont.setPointSize(timeFont.pointSize() - 1);
painter->setFont(timeFont);
painter->setPen(option.palette.text().color().lighter(150));
QString timeStr = index.data(MsgTimeRole).toString();
QRect timeR = QRect(L.bubbleRect.right() - kPadH - 50, L.voicePlayBtnRect.bottom() + 4, 50, 16);
painter->drawText(timeR, Qt::AlignRight | Qt::AlignVCenter, timeStr);
painter->restore();
return;
}
auto *cv = qobject_cast<const ChatView *>(option.widget);
bool singleTextSel = false;
bool multiRowSel = false;