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

+100
View File
@@ -0,0 +1,100 @@
#include "voicemessageencoder.h"
extern "C" {
#include "../../../lib/opus_codec.h"
}
#include <QDebug>
#include <QDir>
#include <QDateTime>
#include <QFile>
#include <cstring>
static const int FRAME_MS = 20;
static const uint32_t OPUS_MAGIC = 0x5355504F; /* "OPUS" */
static const int MAX_PACKET = 4000;
int VoiceEncoder::encodeToFile(const std::vector<int16_t>& pcm, int sampleRate, int channels,
const QString& filePath, float& outDurationSec) {
if (pcm.empty() || sampleRate <= 0 || channels <= 0) {
qWarning("VoiceEncoder: invalid input (pcm=%zu rate=%d ch=%d)", pcm.size(), sampleRate, channels);
return -1;
}
int frameSamples = sampleRate * FRAME_MS / 1000;
size_t totalSamples = pcm.size() / channels;
if (totalSamples < (size_t)frameSamples) return -1;
opus_codec_encoder_t* enc = opus_codec_encoder_create(sampleRate, channels);
if (!enc) {
qWarning("VoiceEncoder: encoder create failed");
return -1;
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
qWarning("VoiceEncoder: cannot open file %s", qPrintable(filePath));
opus_codec_encoder_destroy(enc);
return -1;
}
/* write header */
uint32_t magic = OPUS_MAGIC;
uint32_t sr = (uint32_t)sampleRate;
uint16_t ch = (uint16_t)channels;
uint16_t fs = (uint16_t)frameSamples;
file.write((const char*)&magic, 4);
file.write((const char*)&sr, 4);
file.write((const char*)&ch, 2);
file.write((const char*)&fs, 2);
int frameCount = 0;
uint8_t packet[MAX_PACKET];
const int16_t* pcmData = pcm.data();
size_t pcmOff = 0;
while (pcmOff + (size_t)frameSamples * channels <= totalSamples) {
int len = opus_codec_encode(enc, pcmData + pcmOff, frameSamples, packet, MAX_PACKET);
pcmOff += (size_t)frameSamples * channels;
if (len > 0) {
uint16_t plen = (uint16_t)len;
file.write((const char*)&plen, 2);
file.write((const char*)packet, len);
frameCount++;
} else if (len < 0) {
qWarning("VoiceEncoder: encode error at frame %d: %d", frameCount, len);
}
}
/* end marker */
uint16_t zero = 0;
file.write((const char*)&zero, 2);
file.close();
outDurationSec = (float)frameCount * FRAME_MS / 1000.0f;
qDebug("VoiceEncoder: wrote %d frames (%.1fs) to %s", frameCount, outDurationSec, qPrintable(filePath));
opus_codec_encoder_destroy(enc);
return frameCount;
}
QString VoiceEncoder::generateFileName() {
QDateTime now = QDateTime::currentDateTime();
QString ts = now.toString("yyyyMMdd-HHmmss");
int rnd = rand() & 0xFFFF;
return QString("voice_%1_%2.opus").arg(ts).arg(rnd, 4, 16, QChar('0'));
}
QString VoiceEncoder::mediaDir(const QString& dbPath, const QString& channelId) {
QDir d(dbPath);
QString mediaPath = d.absoluteFilePath("media/" + channelId);
return mediaPath;
}
void VoiceEncoder::ensureMediaDir(const QString& dir) {
QDir d;
if (!d.mkpath(dir)) {
qWarning("VoiceEncoder: cannot create media dir %s", qPrintable(dir));
}
}