media: Ed25519-подписи блоков, local_attrs, MediaBlockReader, файловые вложения
This commit is contained in:
1 parent
6b765de5cc
commit
d4a396f888
20 files changed
+851
-70
No files matched your search
@@ -42,6 +42,41 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "")
|
||||
add_subdirectory(zxing-cpp)
|
||||
set_target_properties(ZXing PROPERTIES AUTOMOC OFF)
|
||||
|
||||
# FFmpeg - custom built shared libraries
|
||||
set(FFMPEG_INSTALL_DIR "${CMAKE_SOURCE_DIR}/ffmpeg/ffmpeg_build/install")
|
||||
set(ENV{PKG_CONFIG_PATH} "${FFMPEG_INSTALL_DIR}/lib/pkgconfig")
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PkgConfig_FOUND)
|
||||
pkg_check_modules(FFMPEG_PC libavcodec libavformat libavutil libswscale libswresample IMPORTED_TARGET)
|
||||
if(FFMPEG_PC_FOUND)
|
||||
set(FFMPEG_FOUND TRUE)
|
||||
set(FFMPEG_TARGET PkgConfig::FFMPEG_PC)
|
||||
set(FFMPEG_INCLUDE_DIRS "${FFMPEG_PC_INCLUDE_DIRS}")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT FFMPEG_FOUND)
|
||||
message(STATUS "pkg-config search failed, trying direct library search")
|
||||
find_path(FFMPEG_INCLUDE_DIR libavcodec/avcodec.h PATHS "${FFMPEG_INSTALL_DIR}/include" NO_DEFAULT_PATH)
|
||||
find_library(AVCODEC_LIBRARY avcodec PATHS "${FFMPEG_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
|
||||
find_library(AVFORMAT_LIBRARY avformat PATHS "${FFMPEG_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
|
||||
find_library(AVUTIL_LIBRARY avutil PATHS "${FFMPEG_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
|
||||
find_library(SWSCALE_LIBRARY swscale PATHS "${FFMPEG_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
|
||||
find_library(SWRESAMPLE_LIBRARY swresample PATHS "${FFMPEG_INSTALL_DIR}/lib" NO_DEFAULT_PATH)
|
||||
if(FFMPEG_INCLUDE_DIR AND AVCODEC_LIBRARY AND AVFORMAT_LIBRARY AND AVUTIL_LIBRARY)
|
||||
set(FFMPEG_FOUND TRUE)
|
||||
set(FFMPEG_INCLUDE_DIRS "${FFMPEG_INCLUDE_DIR}")
|
||||
set(FFMPEG_LIBRARIES "${AVCODEC_LIBRARY}" "${AVFORMAT_LIBRARY}" "${AVUTIL_LIBRARY}" "${SWSCALE_LIBRARY}" "${SWRESAMPLE_LIBRARY}")
|
||||
message(STATUS "FFmpeg found via direct search")
|
||||
endif()
|
||||
endif()
|
||||
if(NOT FFMPEG_FOUND)
|
||||
message(FATAL_ERROR "FFmpeg not found at ${FFMPEG_INSTALL_DIR}. Run: cd ffmpeg && ./build_ffmpeg.sh")
|
||||
endif()
|
||||
if(UNIX AND NOT APPLE)
|
||||
set(CMAKE_BUILD_RPATH "${FFMPEG_INSTALL_DIR}/lib")
|
||||
set(CMAKE_INSTALL_RPATH "${FFMPEG_INSTALL_DIR}/lib")
|
||||
endif()
|
||||
|
||||
# uTun core library (C, built from ../src/ and ../lib/)
|
||||
add_subdirectory(libutun)
|
||||
|
||||
@@ -80,6 +115,7 @@ add_executable(chatgui
|
||||
src/audiocompressor.cpp
|
||||
src/voicemessageencoder.cpp
|
||||
src/voiceplayback.cpp
|
||||
src/media_blocks.cpp
|
||||
src/audiodevicesettingspage.cpp
|
||||
transport/utun_node.cpp
|
||||
transport/node_config.cpp
|
||||
@@ -91,13 +127,13 @@ add_executable(chatgui
|
||||
resources/chatgui.qrc
|
||||
)
|
||||
|
||||
target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include)
|
||||
target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/../../lib ${CMAKE_SOURCE_DIR}/../../src ${CMAKE_SOURCE_DIR}/db ${CMAKE_SOURCE_DIR}/../../lib/libopus/include ${FFMPEG_INCLUDE_DIRS})
|
||||
target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1 USE_SQLITE)
|
||||
set_source_files_properties(../../lib/sqlite3.c transport/miniaudio_impl.c PROPERTIES LANGUAGE C)
|
||||
if(WIN32)
|
||||
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread)
|
||||
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
||||
else()
|
||||
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread dl)
|
||||
target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread dl ${FFMPEG_TARGET} ${FFMPEG_LIBRARIES})
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -170,7 +170,7 @@ QString DbManager::getChannelLastMessage(const QString& channelId) const {
|
||||
QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
QList<MessageRow> list;
|
||||
QString sql = QStringLiteral(
|
||||
"SELECT id, node_id, data, timestamp, chain_hash, author_signature"
|
||||
"SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs"
|
||||
" FROM \"%1\" ORDER BY timestamp DESC, author_signature DESC LIMIT ?").arg(msgTableName(chId));
|
||||
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
||||
if (!stmt) return list;
|
||||
@@ -190,6 +190,7 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
m.datahash = 0;
|
||||
m.chainHash = colBlob(stmt, 4);
|
||||
m.signature = colBlob(stmt, 5);
|
||||
m.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6));
|
||||
m.isOutgoing = (m.authorNodeId == myId);
|
||||
m.isRead = true;
|
||||
list.prepend(m);
|
||||
@@ -201,7 +202,7 @@ QList<MessageRow> DbManager::getMessages(const QString& chId, int limit) const {
|
||||
QList<MessageRow> DbManager::getMessagesSince(const QString& chId, qint64 sinceId) const {
|
||||
QList<MessageRow> list;
|
||||
QString sql = QStringLiteral(
|
||||
"SELECT id, node_id, data, timestamp, chain_hash, author_signature"
|
||||
"SELECT id, node_id, data, timestamp, chain_hash, author_signature, local_attrs"
|
||||
" FROM \"%1\" WHERE id > ? ORDER BY id ASC").arg(msgTableName(chId));
|
||||
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
|
||||
if (!stmt) return list;
|
||||
@@ -219,6 +220,7 @@ QList<MessageRow> DbManager::getMessagesSince(const QString& chId, qint64 sinceI
|
||||
m.datahash = 0;
|
||||
m.chainHash = colBlob(stmt, 4);
|
||||
m.signature = colBlob(stmt, 5);
|
||||
m.localAttrs = QByteArray((const char*)sqlite3_column_text(stmt, 6));
|
||||
m.isOutgoing = (m.authorNodeId == myId);
|
||||
m.isRead = true;
|
||||
list.append(m);
|
||||
|
||||
@@ -36,6 +36,7 @@ struct MessageRow {
|
||||
uint64_t datahash = 0;
|
||||
QByteArray chainHash;
|
||||
QByteArray signature;
|
||||
QByteArray localAttrs;
|
||||
bool isOutgoing = false;
|
||||
bool isRead = false;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "emojipanel.h"
|
||||
#include "audiorecorder.h"
|
||||
#include "voicemessageencoder.h"
|
||||
#include "media_blocks.h"
|
||||
#include "debug_ui.h"
|
||||
#include <QDebug>
|
||||
#include <QTextEdit>
|
||||
@@ -53,15 +54,10 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par
|
||||
layout->setContentsMargins(8, 4, 8, 4);
|
||||
layout->setSpacing(4);
|
||||
|
||||
auto *attachBtn = makeIconBtn(QString::fromUtf8("\xF0\x9F\x93\x8E"),
|
||||
QString::fromUtf8("Прикрепить"), this);
|
||||
connect(attachBtn, &QPushButton::clicked, this, [this]() {
|
||||
QString path = QFileDialog::getOpenFileName(this, QString::fromUtf8("Прикрепить файл"));
|
||||
if (!path.isEmpty()) {
|
||||
qDebug() << "Attached:" << path;
|
||||
}
|
||||
});
|
||||
layout->addWidget(wrapBottomBtn(attachBtn, 3), 0, Qt::AlignBottom);
|
||||
m_attachBtn = makeIconBtn(QString::fromUtf8("\xF0\x9F\x93\x8E"),
|
||||
QString::fromUtf8("Прикрепить"), this);
|
||||
connect(m_attachBtn, &QPushButton::clicked, this, &InputBar::onAttachClicked);
|
||||
layout->addWidget(wrapBottomBtn(m_attachBtn, 3), 0, Qt::AlignBottom);
|
||||
|
||||
m_input->setAcceptRichText(false);
|
||||
m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
@@ -233,30 +229,61 @@ void InputBar::onPttReleased() {
|
||||
qPrintable(mediaPath), QDir(mediaPath).exists(), qPrintable(m_channelIdForRecord));
|
||||
|
||||
QString fileName = VoiceEncoder::generateFileName();
|
||||
QString filePath = mediaPath + "/" + fileName;
|
||||
QString tempPath = mediaPath + "/" + fileName;
|
||||
float durationSec = 0;
|
||||
int frames = VoiceEncoder::encodeToFile(pcm, 48000, 1, filePath, durationSec);
|
||||
if (frames > 0) {
|
||||
m_voiceFilePath = filePath;
|
||||
|
||||
const auto& wf = m_recorder->waveformLevels();
|
||||
QString wfStr;
|
||||
int N = (int)wf.size();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int s = i * N / 100, e = (i + 1) * N / 100;
|
||||
if (s >= N) s = N - 1;
|
||||
if (e <= s) e = s + 1;
|
||||
if (e > N) e = N;
|
||||
float sum = 0; int cnt = 0;
|
||||
for (int j = s; j < e; j++) { sum += wf[j]; cnt++; }
|
||||
float v = cnt > 0 ? sum / cnt : 0;
|
||||
if (i > 0) wfStr += ',';
|
||||
wfStr += QString::number(v, 'f', 3);
|
||||
}
|
||||
GUI_INFO("InputBar: voice message ready: %s (%.1fs) wf=%d levels",
|
||||
qPrintable(filePath), durationSec, N);
|
||||
emit sendVoiceMessage(filePath, durationMs, wfStr);
|
||||
int frames = VoiceEncoder::encodeToFile(pcm, 48000, 1, tempPath, durationSec);
|
||||
if (frames <= 0) {
|
||||
QFile::remove(tempPath);
|
||||
return;
|
||||
}
|
||||
|
||||
MediaBlockInfo blk = media_split_to_blocks(tempPath, mediaPath, true);
|
||||
if (blk.numBlocks <= 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar: block split failed");
|
||||
return;
|
||||
}
|
||||
|
||||
m_voiceFilePath = QString("%1/%2_%3_%4.%5").arg(mediaPath, blk.dt, blk.basename, blk.suffix, blk.ext);
|
||||
|
||||
const auto& wf = m_recorder->waveformLevels();
|
||||
QString wfStr;
|
||||
int N = (int)wf.size();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
int s = i * N / 100, e = (i + 1) * N / 100;
|
||||
if (s >= N) s = N - 1;
|
||||
if (e <= s) e = s + 1;
|
||||
if (e > N) e = N;
|
||||
float sum = 0; int cnt = 0;
|
||||
for (int j = s; j < e; j++) { sum += wf[j]; cnt++; }
|
||||
float v = cnt > 0 ? sum / cnt : 0;
|
||||
if (i > 0) wfStr += ',';
|
||||
wfStr += QString::number(v, 'f', 3);
|
||||
}
|
||||
GUI_INFO("InputBar: voice message ready: dt=%s stem=%s blocks=%d dur=%.1fs",
|
||||
qPrintable(blk.dt), qPrintable(blk.basename), blk.numBlocks, durationSec);
|
||||
emit sendVoiceMessage(blk.dt, blk.basename, blk.suffix, blk.ext,
|
||||
blk.numBlocks, blk.blockSize, durationMs, wfStr);
|
||||
}
|
||||
|
||||
void InputBar::onAttachClicked() {
|
||||
if (m_channelIdForRecord.isEmpty()) return;
|
||||
QString srcPath = QFileDialog::getOpenFileName(this, QString::fromUtf8("Прикрепить файл"));
|
||||
if (srcPath.isEmpty()) return;
|
||||
|
||||
QFileInfo fi(srcPath);
|
||||
if (fi.size() == 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar::onAttachClicked: empty file"); return; }
|
||||
|
||||
QString mediaPath = m_mediaDirBase + "/" + m_channelIdForRecord;
|
||||
VoiceEncoder::ensureMediaDir(mediaPath);
|
||||
QDir().mkpath(mediaPath);
|
||||
|
||||
MediaBlockInfo blk = media_split_to_blocks(srcPath, mediaPath, false);
|
||||
if (blk.numBlocks <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar::onAttachClicked: split failed"); return; }
|
||||
|
||||
GUI_INFO("InputBar::onAttachClicked: file=%s dt=%s blocks=%d",
|
||||
qPrintable(fi.fileName()), qPrintable(blk.dt), blk.numBlocks);
|
||||
emit sendFileAttachment(blk.dt, blk.basename, blk.suffix, blk.ext,
|
||||
blk.numBlocks, blk.blockSize, fi.fileName());
|
||||
}
|
||||
|
||||
void InputBar::resizeEvent(QResizeEvent* event) {
|
||||
|
||||
@@ -19,7 +19,14 @@ public:
|
||||
|
||||
signals:
|
||||
void sendMessage(const QString &text);
|
||||
void sendVoiceMessage(const QString &filePath, int durationMs, const QString& waveformStr);
|
||||
void sendVoiceMessage(const QString& mediaDt, const QString& mediaBasename,
|
||||
const QString& mediaSuffix, const QString& mediaExt,
|
||||
int numBlocks, qint64 blockSize,
|
||||
int durationMs, const QString& waveformStr);
|
||||
void sendFileAttachment(const QString& mediaDt, const QString& mediaBasename,
|
||||
const QString& mediaSuffix, const QString& mediaExt,
|
||||
int numBlocks, qint64 blockSize,
|
||||
const QString& displayName);
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
@@ -30,6 +37,7 @@ private slots:
|
||||
void adjustInputHeight();
|
||||
void onPttPressed();
|
||||
void onPttReleased();
|
||||
void onAttachClicked();
|
||||
void updateRecordingUI(int ms);
|
||||
void showRecordOverlay();
|
||||
void hideRecordOverlay();
|
||||
@@ -38,6 +46,7 @@ private:
|
||||
QTextEdit *m_input;
|
||||
QPushButton *m_emojiBtn;
|
||||
QPushButton *m_pttBtn;
|
||||
QPushButton *m_attachBtn = nullptr;
|
||||
EmojiPanel *m_emojiPanel;
|
||||
AudioRecorder *m_recorder;
|
||||
QWidget *m_recordOverlay = nullptr;
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
#include "media_blocks.h"
|
||||
|
||||
#include "../../lib/debug_config.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QDateTime>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
/* ─── MediaBlockReader ─── */
|
||||
|
||||
MediaBlockReader::MediaBlockReader() {}
|
||||
|
||||
void MediaBlockReader::initFromAttrs(const QString& mediaDir,
|
||||
const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
const QJsonObject& localAttrs,
|
||||
qint64 blockSize, qint64 totalSize, int totalBlocks)
|
||||
{
|
||||
m_mediaDir = mediaDir; m_dt = dt; m_basename = basename;
|
||||
m_suffix = suffix; m_ext = ext;
|
||||
m_blockSize = blockSize; m_totalSize = totalSize; m_totalBlocks = totalBlocks;
|
||||
m_pos = 0; m_valid = true;
|
||||
|
||||
QString st = localAttrs.value("st").toString();
|
||||
if (st == "fl") {
|
||||
m_mode = SINGLE_FILE;
|
||||
QString fp = localAttrs.value("fp").toString();
|
||||
m_filePath = mediaDir + "/" + fp;
|
||||
m_fileSize = totalSize;
|
||||
QFileInfo fi(m_filePath);
|
||||
m_complete = fi.exists() && fi.size() == totalSize;
|
||||
} else {
|
||||
m_mode = FRAGMENTED;
|
||||
QJsonArray bl = localAttrs.value("bl").toArray();
|
||||
for (const auto& v : bl) m_blocks.append(v.toInt());
|
||||
m_complete = (m_blocks.size() == totalBlocks);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: mode=%s complete=%d path=%s blocks=%d/%d",
|
||||
m_mode == SINGLE_FILE ? "single" : "frag", m_complete,
|
||||
qPrintable(debugPath()), m_blocks.size(), totalBlocks);
|
||||
}
|
||||
|
||||
MediaBlockReader MediaBlockReader::fromLegacyFile(const QString& filePath) {
|
||||
MediaBlockReader r;
|
||||
r.m_mode = SINGLE_FILE;
|
||||
r.m_filePath = filePath;
|
||||
r.m_valid = true;
|
||||
QFileInfo fi(filePath);
|
||||
r.m_fileSize = fi.size();
|
||||
r.m_complete = fi.exists() && r.m_fileSize > 0;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: legacy file=%s size=%lld valid=%d",
|
||||
qPrintable(filePath), (long long)r.m_fileSize, r.m_valid);
|
||||
return r;
|
||||
}
|
||||
|
||||
qint64 MediaBlockReader::blockSizeForN(int n) const {
|
||||
if (n < m_totalBlocks - 1) return m_blockSize;
|
||||
qint64 last = m_totalSize - (m_totalBlocks - 1) * m_blockSize;
|
||||
if (last <= 0) last = m_totalSize;
|
||||
return last;
|
||||
}
|
||||
|
||||
QString MediaBlockReader::blockFilePath(int n) const {
|
||||
return QString("%1/%2_%3_%4_%5.%6")
|
||||
.arg(m_mediaDir, m_dt).arg(n).arg(m_basename, m_suffix, m_ext);
|
||||
}
|
||||
|
||||
QByteArray MediaBlockReader::readAll() {
|
||||
if (!m_valid || !m_complete || m_mode == NONE) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: invalid/incomplete");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (m_mode == SINGLE_FILE) {
|
||||
QFile f(m_filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: cannot open %s", qPrintable(m_filePath));
|
||||
return {};
|
||||
}
|
||||
QByteArray data = f.readAll();
|
||||
m_pos = data.size();
|
||||
return data;
|
||||
}
|
||||
|
||||
QByteArray data; data.resize(m_totalSize);
|
||||
qint64 offset = 0;
|
||||
for (int i = 0; i < m_blocks.size(); i++) {
|
||||
int n = m_blocks[i];
|
||||
QString path = blockFilePath(n);
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::readAll: cannot open block %d: %s", n, qPrintable(path));
|
||||
return {};
|
||||
}
|
||||
qint64 sz = blockSizeForN(n);
|
||||
f.read(data.data() + offset, sz);
|
||||
offset += sz;
|
||||
}
|
||||
m_pos = data.size();
|
||||
return data;
|
||||
}
|
||||
|
||||
QByteArray MediaBlockReader::readRange(qint64 offset, qint64 size) {
|
||||
if (!m_valid || offset < 0 || size <= 0 || m_mode == NONE) return {};
|
||||
if (offset >= m_totalSize) return {};
|
||||
|
||||
if (offset + size > m_totalSize) size = m_totalSize - offset;
|
||||
|
||||
if (m_mode == SINGLE_FILE) {
|
||||
QFile f(m_filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) return {};
|
||||
f.seek(offset);
|
||||
QByteArray data = f.read(size);
|
||||
m_pos = offset + data.size();
|
||||
return data;
|
||||
}
|
||||
|
||||
int startBlock = offset / m_blockSize;
|
||||
QByteArray data; data.resize(size);
|
||||
qint64 copied = 0;
|
||||
qint64 remaining = size;
|
||||
int currentN = startBlock;
|
||||
|
||||
while (remaining > 0 && currentN < m_totalBlocks && m_blocks.contains(currentN)) {
|
||||
qint64 blockStart = (qint64)currentN * m_blockSize;
|
||||
qint64 blockSize = blockSizeForN(currentN);
|
||||
qint64 inBlockOffset = offset + copied - blockStart;
|
||||
if (inBlockOffset < 0) inBlockOffset = 0;
|
||||
qint64 toRead = qMin(remaining, blockSize - inBlockOffset);
|
||||
if (toRead <= 0) break;
|
||||
|
||||
QString path = blockFilePath(currentN);
|
||||
QFile f(path);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
f.seek(inBlockOffset);
|
||||
f.read(data.data() + copied, toRead);
|
||||
}
|
||||
copied += toRead;
|
||||
remaining -= toRead;
|
||||
currentN++;
|
||||
}
|
||||
m_pos = offset + copied;
|
||||
if (copied < size) data.resize(copied);
|
||||
return data;
|
||||
}
|
||||
|
||||
bool MediaBlockReader::seek(qint64 pos) {
|
||||
if (!m_valid || pos < 0 || pos > m_totalSize) return false;
|
||||
m_pos = pos;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MediaBlockReader::atEnd() const {
|
||||
return m_pos >= m_totalSize;
|
||||
}
|
||||
|
||||
bool MediaBlockReader::saveToFile(const QString& destPath) {
|
||||
if (!m_valid || !m_complete) return false;
|
||||
|
||||
QByteArray data = readAll();
|
||||
if (data.isEmpty()) return false;
|
||||
|
||||
QFile f(destPath);
|
||||
if (!f.open(QIODevice::WriteOnly)) return false;
|
||||
f.write(data);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: saved to %s (%lld bytes)", qPrintable(destPath), (long long)data.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
QString MediaBlockReader::assembleAndCleanup() {
|
||||
if (!m_valid || m_mode != FRAGMENTED || !m_complete) return {};
|
||||
|
||||
QString assembledPath = QString("%1/%2_%3_%4.%5")
|
||||
.arg(m_mediaDir, m_dt, m_basename, m_suffix, m_ext);
|
||||
|
||||
QFile af(assembledPath);
|
||||
if (!af.open(QIODevice::WriteOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "MediaBlockReader::assemble: cannot create %s", qPrintable(assembledPath));
|
||||
return {};
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_blocks.size(); i++) {
|
||||
int n = m_blocks[i];
|
||||
QString path = blockFilePath(n);
|
||||
QFile bf(path);
|
||||
if (bf.open(QIODevice::ReadOnly)) {
|
||||
af.write(bf.readAll());
|
||||
bf.close();
|
||||
QFile::remove(path);
|
||||
}
|
||||
}
|
||||
af.close();
|
||||
|
||||
m_mode = SINGLE_FILE;
|
||||
m_filePath = assembledPath;
|
||||
m_fileSize = m_totalSize;
|
||||
m_blocks.clear();
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "MediaBlockReader: assembled %s", qPrintable(assembledPath));
|
||||
return assembledPath;
|
||||
}
|
||||
|
||||
QJsonObject MediaBlockReader::localAttrsAfterAssembly() const {
|
||||
QJsonObject obj;
|
||||
obj["st"] = "fl";
|
||||
obj["fp"] = QString("%1_%2_%3.%4").arg(m_dt, m_basename, m_suffix, m_ext);
|
||||
QJsonArray bl;
|
||||
for (int i = 0; i < m_totalBlocks; i++) bl.append(i);
|
||||
obj["bl"] = bl;
|
||||
return obj;
|
||||
}
|
||||
|
||||
QString MediaBlockReader::debugPath() const {
|
||||
if (m_mode == SINGLE_FILE) return m_filePath;
|
||||
if (m_mode == FRAGMENTED) return QString("%1/%2_*_%3_%4.%5")
|
||||
.arg(m_mediaDir, m_dt, m_basename, m_suffix, m_ext);
|
||||
return "none";
|
||||
}
|
||||
|
||||
/* ─── media_split_to_blocks ─── */
|
||||
|
||||
qint64 media_calc_block_size(qint64 fileSize) {
|
||||
if (fileSize <= 0) return 0;
|
||||
qint64 target = (fileSize + 29) / 30; /* ceil(file_size / 30) */
|
||||
if (target < 10 * 1024 * 1024) target = 10 * 1024 * 1024;
|
||||
if (target > 25 * 1024 * 1024) target = 25 * 1024 * 1024;
|
||||
return target;
|
||||
}
|
||||
|
||||
MediaBlockInfo media_split_to_blocks(const QString& srcFile,
|
||||
const QString& mediaDir,
|
||||
bool removeSrc)
|
||||
{
|
||||
MediaBlockInfo info;
|
||||
QFileInfo fi(srcFile);
|
||||
if (!fi.exists()) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: src not found: %s", qPrintable(srcFile));
|
||||
return info;
|
||||
}
|
||||
|
||||
info.fileSize = fi.size();
|
||||
if (info.fileSize == 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: empty file: %s", qPrintable(srcFile));
|
||||
return info;
|
||||
}
|
||||
|
||||
info.blockSize = media_calc_block_size(info.fileSize);
|
||||
info.numBlocks = (int)((info.fileSize + info.blockSize - 1) / info.blockSize);
|
||||
info.ext = fi.suffix();
|
||||
info.basename = fi.completeBaseName();
|
||||
|
||||
QDateTime now = QDateTime::currentDateTimeUtc();
|
||||
info.dt = now.toString("yyyyMMdd-HHmmss");
|
||||
uint16_t rnd = QRandomGenerator::global()->generate() & 0xFFFF;
|
||||
info.suffix = QString::number(rnd, 16).rightJustified(4, '0');
|
||||
|
||||
QDir().mkpath(mediaDir);
|
||||
|
||||
QFile src(srcFile);
|
||||
if (!src.open(QIODevice::ReadOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: cannot open src: %s", qPrintable(srcFile));
|
||||
return info;
|
||||
}
|
||||
|
||||
qint64 remaining = info.fileSize;
|
||||
for (int n = 0; n < info.numBlocks; n++) {
|
||||
qint64 chunk = qMin(remaining, info.blockSize);
|
||||
QString path = QString("%1/%2_%3_%4_%5.%6")
|
||||
.arg(mediaDir, info.dt).arg(n).arg(info.basename, info.suffix, info.ext);
|
||||
QFile dst(path);
|
||||
if (!dst.open(QIODevice::WriteOnly)) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: cannot create block %d: %s", n, qPrintable(path));
|
||||
src.close(); return info;
|
||||
}
|
||||
dst.write(src.read(chunk));
|
||||
dst.close();
|
||||
remaining -= chunk;
|
||||
}
|
||||
src.close();
|
||||
|
||||
if (removeSrc) QFile::remove(srcFile);
|
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_split_to_blocks: %s -> %d blocks (%lld bytes each) in %s",
|
||||
qPrintable(fi.fileName()), info.numBlocks, (long long)info.blockSize, qPrintable(mediaDir));
|
||||
return info;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QList>
|
||||
#include <QFile>
|
||||
#include <QJsonObject>
|
||||
#include <cstdint>
|
||||
|
||||
class MediaBlockReader {
|
||||
public:
|
||||
MediaBlockReader();
|
||||
|
||||
void initFromAttrs(const QString& mediaDir,
|
||||
const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
const QJsonObject& localAttrs,
|
||||
qint64 blockSize, qint64 totalSize, int totalBlocks);
|
||||
|
||||
static MediaBlockReader fromLegacyFile(const QString& filePath);
|
||||
|
||||
bool isValid() const { return m_valid; }
|
||||
bool isComplete() const { return m_complete; }
|
||||
bool isSingleFile() const { return m_mode == SINGLE_FILE; }
|
||||
bool isFragmented() const { return m_mode == FRAGMENTED; }
|
||||
qint64 totalSize() const { return m_totalSize; }
|
||||
|
||||
QByteArray readAll();
|
||||
QByteArray readRange(qint64 offset, qint64 size);
|
||||
bool seek(qint64 pos);
|
||||
qint64 pos() const { return m_pos; }
|
||||
bool atEnd() const;
|
||||
|
||||
bool saveToFile(const QString& destPath);
|
||||
QString assembleAndCleanup();
|
||||
QJsonObject localAttrsAfterAssembly() const;
|
||||
|
||||
QString debugPath() const;
|
||||
|
||||
private:
|
||||
enum Mode { NONE, SINGLE_FILE, FRAGMENTED };
|
||||
Mode m_mode = NONE;
|
||||
bool m_valid = false;
|
||||
bool m_complete = false;
|
||||
qint64 m_pos = 0;
|
||||
|
||||
QString m_filePath;
|
||||
qint64 m_fileSize = 0;
|
||||
|
||||
QString m_mediaDir, m_dt, m_basename, m_suffix, m_ext;
|
||||
QList<int> m_blocks;
|
||||
qint64 m_blockSize = 0;
|
||||
qint64 m_totalSize = 0;
|
||||
int m_totalBlocks = 0;
|
||||
|
||||
qint64 blockSizeForN(int n) const;
|
||||
QString blockFilePath(int n) const;
|
||||
};
|
||||
|
||||
struct MediaBlockInfo {
|
||||
QString dt;
|
||||
QString basename;
|
||||
QString suffix;
|
||||
QString ext;
|
||||
int numBlocks = 0;
|
||||
qint64 fileSize = 0;
|
||||
qint64 blockSize = 0;
|
||||
};
|
||||
|
||||
qint64 media_calc_block_size(qint64 fileSize);
|
||||
MediaBlockInfo media_split_to_blocks(const QString& srcFile,
|
||||
const QString& mediaDir,
|
||||
bool removeSrc = true);
|
||||
@@ -20,7 +20,12 @@ enum MessageDataRole {
|
||||
MsgVoicePlayStateRole = Qt::UserRole + 15,
|
||||
MsgVoicePlayPosRole = Qt::UserRole + 16,
|
||||
MsgVoiceWaveformRole = Qt::UserRole + 17,
|
||||
MsgAuthorNodeIdRole = Qt::UserRole + 20,
|
||||
MsgVoiceFileSizeRole = Qt::UserRole + 18,
|
||||
MsgVoiceBlockSizeRole = Qt::UserRole + 19,
|
||||
MsgAuthorNodeIdRole = Qt::UserRole + 20,
|
||||
MsgVoiceNumBlocksRole = Qt::UserRole + 21,
|
||||
MsgVoiceSigsRole = Qt::UserRole + 22,
|
||||
MsgFileDisplayNameRole = Qt::UserRole + 23,
|
||||
};
|
||||
|
||||
class MessageDelegate : public QStyledItemDelegate {
|
||||
|
||||
@@ -87,8 +87,7 @@ static QString contentTypeFromJson(const QByteArray& data) {
|
||||
|
||||
static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
|
||||
const QString& contentType, const QString& mediaDirBase,
|
||||
const QString& channelId) {
|
||||
(void)channelId;
|
||||
const QString& channelId, const QByteArray& localAttrs) {
|
||||
QString ct = contentType;
|
||||
if (ct.isEmpty()) ct = contentTypeFromJson(data);
|
||||
if (ct.isEmpty()) ct = "text/plain";
|
||||
@@ -97,14 +96,33 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
|
||||
if (ct == "audio/opus") {
|
||||
QString raw = QString::fromUtf8(data);
|
||||
int pipe1 = raw.indexOf('|');
|
||||
if (pipe1 < 0 && raw.startsWith('{')) {
|
||||
/* old double-nested JSON from before the fix — skip */
|
||||
return;
|
||||
}
|
||||
if (pipe1 < 0 && raw.startsWith('{')) return;
|
||||
int pipe2 = raw.indexOf('|', pipe1 + 1);
|
||||
QString fname = pipe1 >= 0 ? raw.left(pipe1) : raw;
|
||||
QString dur = pipe2 >= 0 ? raw.mid(pipe1 + 1, pipe2 - pipe1 - 1) : raw.mid(pipe1 + 1);
|
||||
QString wfData = pipe2 >= 0 ? raw.mid(pipe2 + 1) : QString();
|
||||
QString rest = pipe2 >= 0 ? raw.mid(pipe2 + 1) : QString();
|
||||
|
||||
/* find end of wf (comma-separated floats), then file metadata */
|
||||
int afterWf = -1;
|
||||
int pipeCount = 0;
|
||||
for (int i = 0; i < rest.size(); i++) {
|
||||
if (rest[i] == '|') { pipeCount++; if (pipeCount >= 4) { afterWf = i; break; } }
|
||||
}
|
||||
QString wfData;
|
||||
if (afterWf >= 0) {
|
||||
wfData = rest.left(afterWf);
|
||||
QString meta = rest.mid(afterWf + 1);
|
||||
QStringList parts = meta.split('|');
|
||||
if (parts.size() >= 4) {
|
||||
item->setData(parts[0].toULongLong(), MsgVoiceFileSizeRole);
|
||||
item->setData(parts[1].toULongLong(), MsgVoiceBlockSizeRole);
|
||||
item->setData(parts[2].toUInt(), MsgVoiceNumBlocksRole);
|
||||
item->setData(parts[3], MsgVoiceSigsRole);
|
||||
}
|
||||
} else {
|
||||
wfData = rest;
|
||||
}
|
||||
|
||||
if (!dur.isEmpty()) item->setData(dur + "s", MsgVoiceDurationRole);
|
||||
if (!fname.isEmpty()) {
|
||||
QString fullPath = mediaDirBase + "/" + channelId + "/" + fname;
|
||||
@@ -122,6 +140,10 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
|
||||
item->setData(wf, MsgVoiceWaveformRole);
|
||||
}
|
||||
}
|
||||
if (ct == "application/octet-stream") {
|
||||
QString fname = QString::fromUtf8(data);
|
||||
item->setData(fname, MsgFileDisplayNameRole);
|
||||
}
|
||||
}
|
||||
|
||||
static void setMsg(QStandardItem *item,
|
||||
@@ -212,12 +234,14 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
|
||||
});
|
||||
|
||||
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString &filePath, int durationMs, const QString& wfStr) {
|
||||
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
int numBlocks, qint64 blockSize,
|
||||
int durationMs, const QString& wfStr) {
|
||||
if (m_currentChannelId.isEmpty()) return;
|
||||
|
||||
QFileInfo fi(filePath);
|
||||
QString fileName = fi.fileName();
|
||||
QByteArray textData = (fileName + "|" +
|
||||
QString displayName = basename + "." + ext;
|
||||
QByteArray textData = (displayName + "|" +
|
||||
QString::number(durationMs / 1000.0, 'f', 1) + "|" + wfStr).toUtf8();
|
||||
auto ts = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
@@ -227,6 +251,38 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
|
||||
m_currentChannelId.toUtf8().constData());
|
||||
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
|
||||
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
|
||||
req->media_num_blocks = (uint32_t)numBlocks;
|
||||
req->data = (uint8_t*)(req + 1);
|
||||
req->data_len = (uint32_t)textData.size();
|
||||
memcpy(req->data, textData.constData(), textData.size());
|
||||
req->timestamp = (uint64_t)ts;
|
||||
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
|
||||
});
|
||||
|
||||
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
int numBlocks, qint64 blockSize,
|
||||
const QString& displayName) {
|
||||
if (m_currentChannelId.isEmpty()) return;
|
||||
|
||||
QByteArray textData = displayName.toUtf8();
|
||||
auto ts = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
struct chat_msg_submit* req = (struct chat_msg_submit*)
|
||||
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
|
||||
memset(req, 0, sizeof(*req));
|
||||
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
|
||||
m_currentChannelId.toUtf8().constData());
|
||||
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
|
||||
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
|
||||
req->media_num_blocks = (uint32_t)numBlocks;
|
||||
req->data = (uint8_t*)(req + 1);
|
||||
req->data_len = (uint32_t)textData.size();
|
||||
memcpy(req->data, textData.constData(), textData.size());
|
||||
@@ -316,7 +372,7 @@ void MessageList::loadChannel(const QString& channelId) {
|
||||
item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole);
|
||||
item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole);
|
||||
setVoiceMessageRoles(item, m.data, m.contentType,
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), channelId);
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), channelId, m.localAttrs);
|
||||
m_model->appendRow(item);
|
||||
}
|
||||
|
||||
@@ -381,7 +437,7 @@ void MessageList::refresh() {
|
||||
item->setData(QVariant::fromValue(m.authorNodeId), MsgAuthorNodeIdRole);
|
||||
item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole);
|
||||
setVoiceMessageRoles(item, m.data, m.contentType,
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId);
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, m.localAttrs);
|
||||
m_model->appendRow(item);
|
||||
while (m_model->rowCount() > 100)
|
||||
m_model->removeRow(0);
|
||||
@@ -416,7 +472,7 @@ void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,
|
||||
item->setData(QVariant::fromValue(authorNodeId), MsgAuthorNodeIdRole);
|
||||
item->setData(isSelf ? 1 : 0, MsgIsOutgoingRole);
|
||||
setVoiceMessageRoles(item, content, QString(),
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId);
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, QByteArray());
|
||||
m_model->appendRow(item);
|
||||
|
||||
if (m_model->rowCount() > 50)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "voiceplayback.h"
|
||||
#include "sound_manager.h"
|
||||
#include "media_blocks.h"
|
||||
|
||||
extern "C" {
|
||||
#include "../../../lib/opus_codec.h"
|
||||
@@ -89,3 +90,63 @@ void VoicePlayer::playOpusFile(const QString& filePath) {
|
||||
pcm.size(), sampleRate, channels, duration);
|
||||
SoundManager::instance()->playRawPcm(pcm.data(), pcm.size() / channels, sampleRate, channels);
|
||||
}
|
||||
|
||||
void VoicePlayer::playOpusFromReader(MediaBlockReader& reader) {
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: %s", qPrintable(reader.debugPath()));
|
||||
std::vector<int16_t> pcm;
|
||||
int sampleRate, channels;
|
||||
float duration;
|
||||
if (decodeOpusFromReader(reader, pcm, sampleRate, channels, duration) <= 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: decode failed");
|
||||
return;
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer::playOpusFromReader: decoded %zu samples %dHz %dch %.1fs",
|
||||
pcm.size(), sampleRate, channels, duration);
|
||||
SoundManager::instance()->playRawPcm(pcm.data(), pcm.size() / channels, sampleRate, channels);
|
||||
}
|
||||
|
||||
int VoicePlayer::decodeOpusFromReader(MediaBlockReader& reader, std::vector<int16_t>& outPcm,
|
||||
int& outSampleRate, int& outChannels, float& outDurationSec) {
|
||||
QByteArray data = reader.readAll();
|
||||
if (data.isEmpty()) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer::decodeOpusFromReader: empty data");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint8_t* ptr = (const uint8_t*)data.constData();
|
||||
size_t len = (size_t)data.size();
|
||||
size_t off = 0;
|
||||
|
||||
if (off + 12 > len) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer: header truncated"); return -1; }
|
||||
uint32_t magic; memcpy(&magic, ptr + off, 4); off += 4;
|
||||
if (magic != OPUS_MAGIC) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer: bad magic %08x", magic); return -1; }
|
||||
uint32_t sr32; memcpy(&sr32, ptr + off, 4); off += 4;
|
||||
uint16_t ch16; memcpy(&ch16, ptr + off, 2); off += 2;
|
||||
uint16_t frameSamples; memcpy(&frameSamples, ptr + off, 2); off += 2;
|
||||
int sampleRate = (int)sr32, channels = (int)ch16;
|
||||
outSampleRate = sampleRate; outChannels = channels;
|
||||
|
||||
opus_codec_decoder_t* dec = opus_codec_decoder_create(sampleRate, channels);
|
||||
if (!dec) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "VoicePlayer: decoder create failed"); return -1; }
|
||||
|
||||
outPcm.clear();
|
||||
int frameCount = 0;
|
||||
while (off + 2 <= len) {
|
||||
uint16_t plen; memcpy(&plen, ptr + off, 2); off += 2;
|
||||
if (plen == 0) break;
|
||||
if (off + plen > len) break;
|
||||
std::vector<int16_t> frameBuf(frameSamples * channels);
|
||||
int decoded = opus_codec_decode(dec, ptr + off, plen, frameBuf.data(), frameSamples);
|
||||
off += plen;
|
||||
if (decoded > 0) {
|
||||
size_t cur = outPcm.size();
|
||||
outPcm.resize(cur + decoded * channels);
|
||||
memcpy(outPcm.data() + cur, frameBuf.data(), decoded * channels * sizeof(int16_t));
|
||||
frameCount++;
|
||||
}
|
||||
}
|
||||
outDurationSec = (float)frameCount * 20.0f / 1000.0f;
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "VoicePlayer: decoded %d frames (%.1fs)", frameCount, outDurationSec);
|
||||
opus_codec_decoder_destroy(dec);
|
||||
return frameCount;
|
||||
}
|
||||
@@ -4,9 +4,14 @@
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
|
||||
class MediaBlockReader;
|
||||
|
||||
class VoicePlayer {
|
||||
public:
|
||||
static void playOpusFile(const QString& filePath);
|
||||
static int decodeOpusFile(const QString& filePath, std::vector<int16_t>& outPcm,
|
||||
int& outSampleRate, int& outChannels, float& outDurationSec);
|
||||
int& outSampleRate, int& outChannels, float& outDurationSec);
|
||||
static void playOpusFromReader(MediaBlockReader& reader);
|
||||
static int decodeOpusFromReader(MediaBlockReader& reader, std::vector<int16_t>& outPcm,
|
||||
int& outSampleRate, int& outChannels, float& outDurationSec);
|
||||
};
|
||||
Reference in new issue
Block a user