You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

118 lines
5.7 KiB

#include "voiceplayback.h"
#include "sound_manager.h"
extern "C" {
#include "../../../lib/opus_codec.h"
}
#include "../../lib/debug_config.h"
#include <QFile>
#include <QDebug>
#include <QThread>
#include <QPointer>
#include <QCoreApplication>
#include <memory>
static const uint32_t OPUS_MAGIC = 0x5355504F;
int VoicePlayer::decodeOpusFile(const QString& filePath, std::vector<int16_t>& outPcm,
int& outSampleRate, int& outChannels, float& outDurationSec) {
outPcm.clear(); outDurationSec = 0; outSampleRate = 0; outChannels = 0;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: open failed path=%s reason=%s", qPrintable(filePath), qPrintable(file.errorString()));
return -1;
}
uint32_t magic = 0, rate = 0;
uint16_t channels = 0, frameSamples = 0;
if (file.read((char*)&magic, 4) != 4 || file.read((char*)&rate, 4) != 4 ||
file.read((char*)&channels, 2) != 2 || file.read((char*)&frameSamples, 2) != 2 || magic != OPUS_MAGIC ||
(rate != 8000 && rate != 12000 && rate != 16000 && rate != 24000 && rate != 48000) ||
(channels != 1 && channels != 2) || frameSamples != rate / 50) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: invalid header path=%s rate=%u channels=%u frame=%u",
qPrintable(filePath), rate, channels, frameSamples);
return -1;
}
std::unique_ptr<opus_codec_decoder_t, decltype(&opus_codec_decoder_destroy)>
decoder(opus_codec_decoder_create((int)rate, channels), opus_codec_decoder_destroy);
if (!decoder) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: decoder allocation failed"); return -1; }
std::vector<int16_t> frame((size_t)frameSamples * channels);
constexpr size_t maxPcmSamples = 256 * 1024 * 1024 / sizeof(int16_t);
int frames = 0;
for (;;) {
uint16_t length = 0;
if (file.read((char*)&length, 2) != 2) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: missing end marker path=%s frame=%d", qPrintable(filePath), frames);
return -1;
}
if (!length) {
if (!frames || !file.atEnd()) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: empty stream or trailing bytes path=%s", qPrintable(filePath));
return -1;
}
break;
}
if (length > 4000 || outPcm.size() > maxPcmSamples - frame.size()) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: packet/PCM limit exceeded path=%s frame=%d size=%u",
qPrintable(filePath), frames, length);
return -1;
}
QByteArray packet = file.read(length);
if (packet.size() != length || opus_codec_decode(decoder.get(), (const uint8_t*)packet.constData(),
length, frame.data(), frameSamples) != frameSamples) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: truncated/invalid packet path=%s frame=%d", qPrintable(filePath), frames);
return -1;
}
outPcm.insert(outPcm.end(), frame.begin(), frame.end()); frames++;
}
outSampleRate = (int)rate; outChannels = channels; outDurationSec = frames / 50.0f;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "voice: decoded path=%s frames=%d duration=%.2f", qPrintable(filePath), frames, outDurationSec);
return frames;
}
static unsigned s_decodeGeneration = 0; /* Только GUI-поток. */
static bool s_decoding = false;
static uint64_t s_playbackId = 0;
void VoicePlayer::cancelPending() { s_decodeGeneration++; }
void VoicePlayer::stop() { cancelPending(); SoundManager::instance()->stopPcm(s_playbackId); s_playbackId = 0; }
bool VoicePlayer::isPlaying() { return SoundManager::instance()->isPcmPlaying(s_playbackId); }
uint64_t VoicePlayer::playbackId() { return s_playbackId; }
bool VoicePlayer::isPreparing() { return s_decoding; }
/* Декодирование и выделение PCM выполняются в worker; GUI принимает готовый буфер. */
void VoicePlayer::playOpusFile(const QString& filePath, QObject* context, std::function<void(float, uint64_t)> ready) {
if (s_decoding) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "voice: decoder busy"); ready(0, 0); return; }
struct Result {
std::shared_ptr<std::vector<int16_t>> pcm = std::make_shared<std::vector<int16_t>>();
int rate = 0, channels = 0;
float duration = 0;
};
auto result = std::make_shared<Result>();
unsigned generation = ++s_decodeGeneration;
s_decoding = true;
QPointer<QObject> receiver(context);
auto* thread = QThread::create([filePath, result]() {
try {
if (decodeOpusFile(filePath, *result->pcm, result->rate, result->channels, result->duration) <= 0)
result->duration = 0;
} catch (const std::exception& error) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "voice: decode failed path=%s reason=%s", qPrintable(filePath), error.what());
result->duration = 0;
}
});
QObject::connect(thread, &QThread::finished, QCoreApplication::instance(), [result, receiver, ready, generation]() {
s_decoding = false;
if (!receiver || generation != s_decodeGeneration) return;
if (result->duration > 0) {
SoundManager::instance()->stopPcm(s_playbackId);
s_playbackId = SoundManager::instance()->playRawPcm(result->pcm->data(),
(int)(result->pcm->size() / result->channels), result->rate, result->channels, result->pcm);
if (!s_playbackId) result->duration = 0;
}
ready(result->duration, s_playbackId);
});
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}