voice playback: miniaudio cursor ground truth + EMA smooth decay
- SoundManager: add currentPcmCursor()/isCurrentPcmPlaying() via ma_sound_get_cursor_in_pcm_frames / ma_sound_is_playing - messagedelegate: replace wall-clock position tracking with real miniaudio cursor, remove 200ms hardcoded delay - messagedelegate: end-of-playback detection via isCurrentPcmPlaying() instead of wall-clock comparison (was killing audio ~200ms early) - messagedelegate: EMA smoothing of displayFrac/displayElapsed (0.15 factor, 16ms timer) - messagedelegate: timer continues after audio stops to animate EMA decay to 0 (not mouse-move dependent) - media_delivery: download progress callback, Ed25519 sig verify, author fallback, content hash check - conn_mgr: already-connected returns OK with callback instead of error - chatgui: voice download-pending auto-play, file download progress bar, media bar colors
This commit is contained in:
1 parent
641c9f574f
commit
51d6c782ac
23 files changed
+1165
-162
No files matched your search
@@ -18,6 +18,9 @@
|
||||
#include <QDateTime>
|
||||
#include <QTimer>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QDesktopServices>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
|
||||
extern "C" {
|
||||
@@ -37,6 +40,8 @@ struct VoicePlayState {
|
||||
qint64 startMs = 0;
|
||||
float durationSec = 0.0f;
|
||||
float pausedPosSec = 0.0f;
|
||||
float displayFrac = 0.0f;
|
||||
float displayElapsed = 0.0f;
|
||||
};
|
||||
static QMap<QString, VoicePlayState> s_voiceStates;
|
||||
static QTimer* s_voiceTimer = nullptr;
|
||||
@@ -45,26 +50,25 @@ static QAbstractItemView* s_voiceView = nullptr;
|
||||
static void ensureVoiceTimer() {
|
||||
if (!s_voiceTimer) {
|
||||
s_voiceTimer = new QTimer();
|
||||
s_voiceTimer->setInterval(100);
|
||||
s_voiceTimer->setInterval(16);
|
||||
QObject::connect(s_voiceTimer, &QTimer::timeout, []() {
|
||||
bool anyPlaying = false;
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ) {
|
||||
if (!it->playing && !it->paused) { ++it; continue; }
|
||||
if (it->paused) { ++it; continue; }
|
||||
if (!it->playing) { ++it; continue; }
|
||||
float elapsed = (now - it->startMs) / 1000.0f;
|
||||
if (elapsed >= it->durationSec && it->durationSec > 0) {
|
||||
bool anyActive = false;
|
||||
SoundManager* sm = SoundManager::instance();
|
||||
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) {
|
||||
if (!it->playing && !it->paused) {
|
||||
if (it->displayFrac > 0.001f || it->displayElapsed > 0.001f) anyActive = true;
|
||||
continue;
|
||||
}
|
||||
if (it->paused) { anyActive = true; continue; }
|
||||
if (!sm->isCurrentPcmPlaying()) {
|
||||
it->playing = false;
|
||||
it->paused = false;
|
||||
SoundManager::instance()->stopCurrentPcm();
|
||||
++it;
|
||||
anyActive = true;
|
||||
} else {
|
||||
anyPlaying = true;
|
||||
++it;
|
||||
anyActive = true;
|
||||
}
|
||||
}
|
||||
if (!anyPlaying) s_voiceTimer->stop();
|
||||
if (!anyActive) s_voiceTimer->stop();
|
||||
if (s_voiceView) s_voiceView->viewport()->update();
|
||||
});
|
||||
}
|
||||
@@ -78,11 +82,18 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
|
||||
if (L.isFile) {
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
if (dlState != 0) return false; /* already downloaded or in progress */
|
||||
qint64 msgId = index.data(MsgIdRole).toLongLong();
|
||||
QString chId = index.data(MsgChannelIdRole).toString();
|
||||
if (chId.isEmpty() || msgId <= 0) return false;
|
||||
|
||||
if (dlState == 2) { /* downloaded: open file */
|
||||
QString fp = index.data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp))
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(fp));
|
||||
return true;
|
||||
}
|
||||
if (dlState != 0) return false; /* downloading — no action */
|
||||
|
||||
if (chId.isEmpty() || msgId <= 0) return false;
|
||||
struct attachment_dl_req* req = (struct attachment_dl_req*)
|
||||
u_malloc(sizeof(struct attachment_dl_req));
|
||||
if (!req) return false;
|
||||
@@ -103,6 +114,25 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
if (!onPlayBtn && !onWaveform) return false;
|
||||
|
||||
QString filePath = index.data(MsgVoiceFileRole).toString();
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
qint64 msgId = index.data(MsgIdRole).toLongLong();
|
||||
QString chId = index.data(MsgChannelIdRole).toString();
|
||||
|
||||
/* not downloaded: start download (play will start on completion) */
|
||||
if (filePath.isEmpty() && dlState != 1) {
|
||||
if (chId.isEmpty() || msgId <= 0) return false;
|
||||
struct attachment_dl_req* req = (struct attachment_dl_req*)
|
||||
u_malloc(sizeof(struct attachment_dl_req));
|
||||
if (!req) return false;
|
||||
memset(req, 0, sizeof(*req));
|
||||
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
|
||||
req->msg_id = msgId;
|
||||
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
|
||||
model->setData(index, true, MsgVoiceDownloadPendingRole);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filePath.isEmpty()) return false;
|
||||
GUI_DEBUG("editorEvent voice click: filePath=%s exists=%d wave=%d",
|
||||
qPrintable(filePath), QFile::exists(filePath), (int)onWaveform);
|
||||
if (filePath.isEmpty()) return false;
|
||||
@@ -128,6 +158,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
st.durationSec = dur;
|
||||
st.startMs = now - (qint64)(targetSec * 1000.0);
|
||||
st.pausedPosSec = targetSec;
|
||||
st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0;
|
||||
st.displayElapsed = targetSec;
|
||||
VoicePlayer::playOpusFile(filePath);
|
||||
int sr = SoundManager::instance()->currentPcmSampleRate();
|
||||
if (sr > 0)
|
||||
@@ -141,6 +173,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
st.pausedPosSec = targetSec;
|
||||
else if (!st.playing)
|
||||
{ st.playing = true; s_voiceTimer->start(); }
|
||||
st.displayFrac = (targetSec > 0 && dur > 0) ? (targetSec / dur) : 0;
|
||||
st.displayElapsed = targetSec;
|
||||
int sr = SoundManager::instance()->currentPcmSampleRate();
|
||||
if (sr > 0)
|
||||
SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr));
|
||||
@@ -172,6 +206,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
st.playing = true;
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
st.startMs = now - (qint64)(st.pausedPosSec * 1000.0);
|
||||
st.displayFrac = (dur > 0) ? (st.pausedPosSec / dur) : 0;
|
||||
st.displayElapsed = st.pausedPosSec;
|
||||
SoundManager::instance()->resumeCurrentPcm();
|
||||
s_voiceTimer->start();
|
||||
model->setData(index, 1, MsgVoicePlayStateRole);
|
||||
@@ -191,6 +227,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
st.pausedPosSec = 0;
|
||||
st.startMs = QDateTime::currentMSecsSinceEpoch();
|
||||
st.durationSec = dur;
|
||||
st.displayFrac = 0;
|
||||
st.displayElapsed = 0;
|
||||
|
||||
VoicePlayer::playOpusFile(filePath);
|
||||
s_voiceTimer->start();
|
||||
@@ -269,14 +307,23 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
|
||||
int wh = 25; // waveform height
|
||||
int th = 14; // text line height
|
||||
L.bubbleWidth = 220;
|
||||
int barW = 3, barPad = 4;
|
||||
int bubbleH = kPadTop + wh + 4 + th + kPadBot;
|
||||
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap;
|
||||
|
||||
int contentEdge = L.isOutgoing
|
||||
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
|
||||
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
|
||||
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth) : contentEdge;
|
||||
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth - barW - barPad) : contentEdge;
|
||||
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH);
|
||||
L.mediaBarRect = QRect(L.isOutgoing ? bx + L.bubbleWidth + 1 : bx - barW - 1, 0, barW, bubbleH);
|
||||
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
int bd = index.data(MsgMediaBlocksDoneRole).toInt();
|
||||
int nb = index.data(MsgMediaNumBlocksRole).toInt();
|
||||
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0;
|
||||
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb;
|
||||
if (dlState == 1) { L.progressBarRect = QRect(bx, bubbleH - 3, L.bubbleWidth, 3); }
|
||||
|
||||
L.voicePlayBtnRect = QRect(bx + kPadH, kPadTop + 4, pbs, pbs);
|
||||
int wx = bx + kPadH + pbs + 4;
|
||||
@@ -302,14 +349,23 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
|
||||
int th = fm.height();
|
||||
int ih = 24; /* icon height */
|
||||
L.bubbleWidth = 230;
|
||||
int barW = 3, barPad = 4;
|
||||
int bubbleH = kPadTop + ih + 2 + th + kPadBot;
|
||||
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap;
|
||||
|
||||
int contentEdge = L.isOutgoing
|
||||
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
|
||||
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
|
||||
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth) : contentEdge;
|
||||
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth - barW - barPad) : contentEdge;
|
||||
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH);
|
||||
L.mediaBarRect = QRect(L.isOutgoing ? bx + L.bubbleWidth + 1 : bx - barW - 1, 0, barW, bubbleH);
|
||||
|
||||
int dlState = index.data(MsgFileDownloadStateRole).toInt();
|
||||
int bd = index.data(MsgMediaBlocksDoneRole).toInt();
|
||||
int nb = index.data(MsgMediaNumBlocksRole).toInt();
|
||||
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0;
|
||||
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb;
|
||||
if (dlState == 1) { L.progressBarRect = QRect(bx, bubbleH - 3, L.bubbleWidth, 3); }
|
||||
|
||||
L.textRect = QRect(bx + kPadH + ih + 8, kPadTop, L.bubbleWidth - 2*kPadH - ih - 8, th);
|
||||
L.statusRect = QRect(bx + kPadH + ih + 8 - 48, kPadTop + th + 2, L.bubbleWidth - 2*kPadH - ih - 8 + 48 - 40, th);
|
||||
@@ -712,36 +768,44 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
if (!L.narrow) drawAvatar(painter, L, index);
|
||||
|
||||
QString filePath = index.data(MsgVoiceFileRole).toString();
|
||||
VoicePlayState st;
|
||||
VoicePlayState copySt;
|
||||
VoicePlayState* st = ©St;
|
||||
if (!filePath.isEmpty() && s_voiceStates.contains(filePath)) {
|
||||
st = s_voiceStates[filePath];
|
||||
st = &s_voiceStates[filePath];
|
||||
} else {
|
||||
int stateVal = index.data(MsgVoicePlayStateRole).toInt();
|
||||
st.playing = (stateVal == 1);
|
||||
copySt.playing = (stateVal == 1);
|
||||
st = ©St;
|
||||
}
|
||||
|
||||
bool isPlaying = st.playing;
|
||||
bool isPaused = st.paused;
|
||||
bool isPlaying = st->playing;
|
||||
bool isPaused = st->paused;
|
||||
|
||||
/* compute elapsed/frac for progress */
|
||||
float elapsed = 0, frac = 0, durSec = st.durationSec;
|
||||
float elapsed = 0, targetFrac = 0, durSec = st->durationSec;
|
||||
if (durSec <= 0) {
|
||||
QString ds = index.data(MsgVoiceDurationRole).toString();
|
||||
ds.remove('s'); durSec = ds.toFloat();
|
||||
}
|
||||
if (isPaused) {
|
||||
elapsed = st.pausedPosSec;
|
||||
elapsed = st->pausedPosSec;
|
||||
} else if (isPlaying) {
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
elapsed = (now - st.startMs) / 1000.0f;
|
||||
SoundManager* sm = SoundManager::instance();
|
||||
if (sm->isCurrentPcmPlaying()) {
|
||||
int sr = sm->currentPcmSampleRate();
|
||||
if (sr > 0) elapsed = (float)((double)sm->currentPcmCursor() / sr);
|
||||
}
|
||||
if (elapsed <= 0) {
|
||||
qint64 now = QDateTime::currentMSecsSinceEpoch();
|
||||
elapsed = (now - st->startMs) / 1000.0f;
|
||||
}
|
||||
if (elapsed > durSec) elapsed = durSec;
|
||||
}
|
||||
if (durSec > 0) {
|
||||
float dispElapsed = elapsed - 0.2f; /* 200ms audio output delay */
|
||||
if (dispElapsed < 0) dispElapsed = 0;
|
||||
frac = dispElapsed / durSec;
|
||||
}
|
||||
if (frac < 0) frac = 0; if (frac > 1) frac = 1;
|
||||
targetFrac = (durSec > 0) ? (elapsed / durSec) : 0;
|
||||
if (targetFrac < 0) targetFrac = 0; if (targetFrac > 1) targetFrac = 1;
|
||||
st->displayFrac += (targetFrac - st->displayFrac) * 0.15f;
|
||||
st->displayElapsed += (elapsed - st->displayElapsed) * 0.15f;
|
||||
float frac = st->displayFrac;
|
||||
|
||||
QRectF playR = L.voicePlayBtnRect;
|
||||
|
||||
@@ -806,7 +870,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
timeFont.setPointSize(timeFont.pointSize() - 2);
|
||||
painter->setFont(timeFont);
|
||||
painter->setPen(option.palette.text().color().lighter(150));
|
||||
int eSec = (int)elapsed, dSec = (int)durSec;
|
||||
int eSec = (int)st->displayElapsed, dSec = (int)durSec;
|
||||
QString durText = QString("%1:%2 / %3:%4")
|
||||
.arg(eSec / 60).arg(eSec % 60, 2, 10, QChar('0'))
|
||||
.arg(dSec / 60).arg(dSec % 60, 2, 10, QChar('0'));
|
||||
@@ -819,6 +883,17 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
painter->drawText(L.voiceTimeRect, Qt::AlignRight | Qt::AlignVCenter,
|
||||
index.data(MsgTimeRole).toString());
|
||||
|
||||
/* media bar (left edge, 3px) */
|
||||
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80);
|
||||
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); }
|
||||
/* progress bar (bottom edge, 3px) */
|
||||
if (L.progressBarRect.isValid()) {
|
||||
painter->setBrush(QColor(0x44, 0x44, 0x44));
|
||||
painter->drawRoundedRect(L.progressBarRect, 2, 2);
|
||||
int pw = (int)(L.progressBarRect.width() * L.progressFrac);
|
||||
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); }
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
return;
|
||||
}
|
||||
@@ -853,12 +928,23 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
painter->setPen(QColor(0x80, 0x80, 0x80));
|
||||
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
|
||||
QString sizeStr = formatFileSize(fsz);
|
||||
QString stateStr = (dlState == 2) ? "Downloaded" : "Tap to download";
|
||||
QString stateStr = (dlState == 2) ? QString::fromUtf8("\xE2\x9C\x85") : (dlState == 1) ? "Downloading..." : "Download";
|
||||
QString info = sizeStr + " \xC2\xB7 " + stateStr;
|
||||
int nameH = QFontMetrics(fileFont).height();
|
||||
QRect sizeRect(L.textRect.left(), L.textRect.top() + nameH, L.textRect.width(), QFontMetrics(sizeFont).height());
|
||||
painter->drawText(sizeRect, Qt::AlignLeft | Qt::AlignVCenter, info);
|
||||
|
||||
/* media bar (left edge, 3px) */
|
||||
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80);
|
||||
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); }
|
||||
/* progress bar (bottom edge, 3px) */
|
||||
if (L.progressBarRect.isValid()) {
|
||||
painter->setBrush(QColor(0x44, 0x44, 0x44));
|
||||
painter->drawRoundedRect(L.progressBarRect, 2, 2);
|
||||
int pw = (int)(L.progressBarRect.width() * L.progressFrac);
|
||||
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); }
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
return;
|
||||
|
||||
|
||||
Reference in new issue
Block a user