#include "voicemessageencoder.h" extern "C" { #include "../../../lib/opus_codec.h" } #include "../../lib/debug_config.h" #include #include #include #include #include static const int FRAME_MS = 20; static const uint32_t OPUS_MAGIC = 0x5355504F; static const int MAX_PACKET = 4000; static int s_preset = 1; /* 0=Low, 1=Standard, 2=High */ static const int s_bitrates[] = {16000, 32000, 64000}; static const int s_complexities[] = {2, 5, 10}; void VoiceEncoder::setPreset(int preset) { if (preset < 0) preset = 0; if (preset > 2) preset = 2; s_preset = preset; } int VoiceEncoder::preset() { return s_preset; } int VoiceEncoder::encodeToFile(const int16_t* pcm, size_t pcmCount, int sampleRate, int channels, const QString& filePath, float& outDurationSec) { if (!pcm || pcmCount == 0 || sampleRate <= 0 || channels <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoiceEncoder: invalid input (pcm=%zu rate=%d ch=%d)", pcmCount, sampleRate, channels); return -1; } int frameSamples = sampleRate * FRAME_MS / 1000; size_t totalSamples = pcmCount / (size_t)channels; if (totalSamples < (size_t)frameSamples) return -1; opus_codec_encoder_t* enc = opus_codec_encoder_create(sampleRate, channels); if (!enc) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoiceEncoder: encoder create failed"); return -1; } opus_codec_encoder_bitrate_set(enc, s_bitrates[s_preset]); opus_codec_encoder_complexity_set(enc, s_complexities[s_preset]); QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoiceEncoder: cannot open file %s (%s)", qPrintable(filePath), qPrintable(file.errorString())); opus_codec_encoder_destroy(enc); return -1; } DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoiceEncoder::encodeToFile: writing to %s pcm=%zu samples preset=%d", qPrintable(filePath), pcmCount, s_preset); /* 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]; size_t pcmOff = 0; while (pcmOff + (size_t)frameSamples * channels <= totalSamples) { int len = opus_codec_encode(enc, pcm + 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) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "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; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "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)) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoiceEncoder: cannot create media dir %s", qPrintable(dir)); } }