feat: waveform display + 3px progress bar in bubble + sliding window RMS

- AudioRecorder: sliding window RMS (avg 5 frames), waveformLevels() getter
- InputBar: downsample RMS to 100 samples, encode as wf0,wf1,... in textData
- MessageList: parse 3rd pipe-separated field as waveform, store in MsgVoiceWaveformRole
- MessageDelegate: new voice layout (16px play btn + 25px waveform + 3px progress bar)
- MessageDelegate: waveform bars colored by progress (blue before, gray after)
- Progress bar integrated at bottom of waveform area as colored fill
This commit is contained in:
Evgeny committed 2026-07-27 09:07:14 +03:00
1 parent 70a0a70b88
commit 05c7b4d47e
7 files changed
+133 -75

No files matched your search

+77 -63
View File
@@ -190,20 +190,26 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
L.isVoice = (contentType == "audio/opus");
if (L.isVoice) {
L.voiceDuration = index.data(MsgVoiceDurationRole).toString();
QFontMetrics fm(option.font);
int lineH = fm.lineSpacing();
L.totalHeight = qMax(lineH * 2 + kGap * 2, kAvatar) + kItemGap;
L.bubbleWidth = 160;
int pbs = 16; // play button size
int wh = 25; // waveform height
int th = 14; // text line height
L.bubbleWidth = 220;
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));
if (!L.isOutgoing)
L.bubbleRect = QRect(contentEdge, 0, L.bubbleWidth, L.totalHeight - kItemGap);
else
L.bubbleRect = QRect(contentEdge - L.bubbleWidth, 0, L.bubbleWidth, L.totalHeight - kItemGap);
L.voicePlayBtnRect = QRect(L.bubbleRect.left() + kPadH, L.bubbleRect.top() + kPadTop + 4, 28, 28);
L.voiceDurationRect = QRect(L.bubbleRect.left() + kPadH + 36, L.bubbleRect.top() + kPadTop + 4,
L.bubbleWidth - kPadH * 2 - 36, 28);
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth) : contentEdge;
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH);
L.voicePlayBtnRect = QRect(bx + kPadH, kPadTop + 4, pbs, pbs);
int wx = bx + kPadH + pbs + 4;
int ww = L.bubbleWidth - 2*kPadH - pbs - 4;
L.voiceWaveformRect = QRect(wx, kPadTop + 1, ww, wh);
L.voiceDurationRect = QRect(wx, kPadTop + wh + 4, ww - 40, th);
L.voiceTimeRect = QRect(wx + ww - 40, kPadTop + wh + 4, 40, th);
if (!L.narrow) {
int avatarY = L.bubbleRect.bottom() - kAvatar;
if (!L.isOutgoing)
@@ -611,14 +617,25 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
bool isPlaying = st.playing;
bool isPaused = st.paused;
/* compute elapsed/frac for progress */
float elapsed = 0, frac = 0, durSec = st.durationSec;
if (isPaused) {
elapsed = st.pausedPosSec;
} else if (isPlaying) {
qint64 now = QDateTime::currentMSecsSinceEpoch();
elapsed = (now - st.startMs) / 1000.0f;
if (elapsed > durSec) elapsed = durSec;
}
if (durSec > 0) frac = elapsed / durSec;
if (frac < 0) frac = 0; if (frac > 1) frac = 1;
QRectF playR = L.voicePlayBtnRect;
/* Play/Pause button */
/* Play/Pause icon (small — 10px triangle) */
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0x33, 0x90, 0xEC));
if (isPlaying || isPaused) {
/* Pause icon (two vertical bars) */
qreal bw = playR.width() / 5.0;
qreal gap = playR.width() / 8.0;
qreal top = playR.center().y() - playR.height() / 3.0;
@@ -626,66 +643,63 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
painter->drawRoundedRect(QRectF(playR.center().x() - gap - bw, top, bw, bot - top), 1, 1);
painter->drawRoundedRect(QRectF(playR.center().x() + gap, top, bw, bot - top), 1, 1);
} else {
/* Play icon (triangle) */
QPainterPath playPath;
QPainterPath path;
qreal cx = playR.center().x() - playR.width() / 6.0;
qreal cy = playR.center().y();
qreal hs = playR.height() / 2.5;
playPath.moveTo(cx, cy - hs);
playPath.lineTo(cx + hs * 1.5, cy);
playPath.lineTo(cx, cy + hs);
playPath.closeSubpath();
painter->drawPath(playPath);
path.moveTo(cx, cy - hs);
path.lineTo(cx + hs * 1.5, cy);
path.lineTo(cx, cy + hs);
path.closeSubpath();
painter->drawPath(path);
}
/* Duration text */
painter->setPen(option.palette.text().color());
painter->setFont(option.font);
if (!L.voiceDuration.isEmpty())
painter->drawText(L.voiceDurationRect, Qt::AlignLeft | Qt::AlignVCenter, L.voiceDuration);
/* Progress bar */
int pbLeft = L.bubbleRect.left() + kPadH;
int pbWidth = L.bubbleWidth - 2 * kPadH;
int pbTop = L.voicePlayBtnRect.bottom() + 6;
int pbHeight = 3;
if ((isPlaying || isPaused) && st.durationSec > 0) {
float elapsed;
if (isPaused) {
elapsed = st.pausedPosSec;
} else {
qint64 now = QDateTime::currentMSecsSinceEpoch();
elapsed = (now - st.startMs) / 1000.0f;
if (elapsed > st.durationSec) elapsed = st.durationSec;
/* Waveform (25px) */
QList<QVariant> wf = index.data(MsgVoiceWaveformRole).toList();
int wfN = wf.size();
QRect wfR = L.voiceWaveformRect;
if (wfN > 0) {
float barW = (float)wfR.width() / wfN;
int ph = wfR.height() - 3; /* leave 3px for progress bar at bottom */
for (int i = 0; i < wfN; i++) {
float v = wf[i].toFloat();
int barH = 1 + (int)(v * (ph - 1));
int x = wfR.left() + (int)(i * barW);
int y = wfR.top() + ph - barH;
int bw = qMax(2, (int)(barW * 0.8f));
bool before = wfN > 0 && (float)i / wfN <= frac;
painter->setBrush(before ? QColor(0x33, 0x90, 0xEC) : QColor(0x33, 0x90, 0xEC, 45));
painter->drawRect(QRect(x, y, bw, barH));
}
float frac = elapsed / st.durationSec;
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0x33, 0x90, 0xEC, 60));
painter->drawRoundedRect(QRect(pbLeft, pbTop, pbWidth, pbHeight), 2, 2);
painter->setBrush(QColor(0x33, 0x90, 0xEC));
painter->drawRoundedRect(QRect(pbLeft, pbTop, (int)(pbWidth * frac), pbHeight), 2, 2);
/* Position text */
QFont timeFont = option.font;
timeFont.setPointSize(timeFont.pointSize() - 2);
painter->setFont(timeFont);
painter->setPen(option.palette.text().color().lighter(150));
int secs = (int)elapsed;
QString posStr = QString("%1:%2").arg(secs / 60).arg(secs % 60, 2, 10, QChar('0'));
QRect posR(pbLeft, pbTop + 4, pbWidth / 2, 14);
painter->drawText(posR, Qt::AlignLeft | Qt::AlignVCenter, posStr);
}
/* Time */
/* Progress bar (3px, at bottom of waveform area) */
int ph = 3;
int pbY = wfR.bottom() - ph;
painter->setBrush(QColor(0x44, 0x44, 0x44));
painter->drawRoundedRect(QRect(wfR.left(), pbY, wfR.width(), ph), 2, 2);
if (frac > 0) {
painter->setBrush(QColor(0x33, 0x90, 0xEC));
painter->drawRoundedRect(QRect(wfR.left(), pbY, (int)(wfR.width() * frac), ph), 2, 2);
}
/* Duration + elapsed on bottom line */
QFont timeFont = option.font;
timeFont.setPointSize(timeFont.pointSize() - 1);
timeFont.setPointSize(timeFont.pointSize() - 2);
painter->setFont(timeFont);
painter->setPen(option.palette.text().color().lighter(150));
QString timeStr = index.data(MsgTimeRole).toString();
QRect timeR = QRect(L.bubbleRect.right() - kPadH - 50, pbTop + 4, 50, 16);
painter->drawText(timeR, Qt::AlignRight | Qt::AlignVCenter, timeStr);
int eSec = (int)elapsed, 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'));
painter->drawText(L.voiceDurationRect, Qt::AlignLeft | Qt::AlignVCenter, durText);
/* Timestamp */
QFont tsFont = option.font;
tsFont.setPointSize(tsFont.pointSize() - 1);
painter->setFont(tsFont);
painter->drawText(L.voiceTimeRect, Qt::AlignRight | Qt::AlignVCenter,
index.data(MsgTimeRole).toString());
painter->restore();
return;