From 4ffa59d96837738465679823da8ebe569d82a530 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 29 Jul 2026 01:43:11 +0300 Subject: [PATCH] unify voice waveform format: base64 wf={100chars};dur={ms}; for both desktop and Android MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - inputbar.cpp: CSV waveform → base64 log-scale (same as voice_recorder.c) - messagelist.cpp: sendVoiceMessage removes displayName|dur_sec| prefix, uses wfStr directly setVoiceMessageRoles: decodes new format wf={base64};dur={ms};|... from data field - messagedelegate.cpp: waveform values now pre-normalized 0..1, no log recalculation needed - VoiceEncoder: no changes needed (was already compatible) Both platforms now store identical format in chat_msg_submit.data: wf=ABCDef...100chars;dur=NNNN;|file_size|block_size|num_blocks|sigs_hex --- tools/chatgui/src/inputbar.cpp | 10 ++-- tools/chatgui/src/messagedelegate.cpp | 9 ++-- tools/chatgui/src/messagelist.cpp | 72 +++++++++++++++------------ 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index a6ae5496..1d88fccf 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -250,7 +250,8 @@ void InputBar::onPttReleased() { 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; + QString wfStr = "wf="; + static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; int N = (int)wf.size(); for (int i = 0; i < 100; i++) { int s = i * N / 100, e = (i + 1) * N / 100; @@ -260,9 +261,12 @@ void InputBar::onPttReleased() { 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); + float db = v > 0.00001f ? 20.0f * log10f(v) : -100.0f; + int idx = (int)((db + 60.0f) / 60.0f * 64.0f); + if (idx < 0) idx = 0; if (idx > 63) idx = 63; + wfStr += b64[idx]; } + wfStr += QString(";dur=%1;").arg(durationMs); 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, diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 1a2539ac..86c97eff 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -710,10 +710,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio float barW = (float)wfR.width() / wfN; int ph = wfR.height() - 3; for (int i = 0; i < wfN; i++) { - float v = wf[i].toFloat(); - float db = v > 0.00001f ? 20.0f * log10f(v) : -100.0f; - float logV = (db + 60.0f) / 60.0f; - if (logV < 0) logV = 0; if (logV > 1) logV = 1; + float logV = wf[i].toFloat(); /* already 0..1 normalized */ int barH = 2 + (int)(logV * (ph - 2)); int x = wfR.left() + (int)(i * barW); int y = wfR.top() + ph - barH; @@ -724,8 +721,8 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio QColor idle(0x70, 0x70, 0x78, 130); painter->setBrush((isPlaying || isPaused) ? (before ? played : unplayed) : idle); painter->drawRect(QRect(x, y, bw, barH)); - if (i == 0) GUI_DEBUG("voice paint: wfN=%d wfW=%d v=%.3f dB=%.1f logV=%.2f barH=%d", - wfN, wfR.width(), v, db, logV, barH); + if (i == 0) GUI_DEBUG("voice paint: wfN=%d wfW=%d logV=%.2f barH=%d", + wfN, wfR.width(), logV, barH); } } diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index f52e219e..53da6569 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -95,23 +97,26 @@ 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('{')) 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 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; } } - } + if (raw.startsWith('{')) return; + + /* unified format: wf={base64};dur={ms};|{file_size}|{block_size}|{num_blocks}|{sigs} */ QString wfData; - if (afterWf >= 0) { - wfData = rest.left(afterWf); - QString meta = rest.mid(afterWf + 1); + int durMs = 0; + + if (raw.startsWith("wf=")) { + int durPos = raw.indexOf(";dur=", 3); + int durEnd = raw.indexOf(';', durPos + 5); + if (durPos > 0 && durEnd > 0) { + wfData = raw.mid(3, durPos - 3); /* 100 base64 chars */ + durMs = raw.mid(durPos + 5, durEnd - durPos - 5).toInt(); + } + } + item->setData(QString::number(durMs / 1000.0, 'f', 1) + "s", MsgVoiceDurationRole); + + /* block metadata after last ; */ + int pipePos = raw.indexOf('|'); + if (pipePos > 0) { + QString meta = raw.mid(pipePos + 1); QStringList parts = meta.split('|'); if (parts.size() >= 4) { item->setData(parts[0].toULongLong(), MsgVoiceFileSizeRole); @@ -119,25 +124,32 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data, 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; + /* file path from local_attrs */ + QJsonObject la = QJsonDocument::fromJson(localAttrs).object(); + QString fp = la.value("fp").toString(); + if (!fp.isEmpty()) { + QString fullPath = mediaDirBase + "/" + channelId + "/" + fp; item->setData(fullPath, MsgVoiceFileRole); - DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "setVoiceMessageRoles: voice file=%s dur=%s wf=%d exists=%d", - qPrintable(fullPath), qPrintable(dur), - wfData.split(',').size(), QFile::exists(fullPath)); - } else { - DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "setVoiceMessageRoles: empty filename, raw='%s'", qPrintable(raw)); } + + /* decode base64 waveform to normalized 0..1 float list */ if (!wfData.isEmpty()) { QList wf; - for (const auto& s : wfData.split(',')) - wf.append(s.toDouble()); + for (int i = 0; i < wfData.size(); i++) { + QChar c = wfData[i]; + int idx = 0; + if (c >= 'A' && c <= 'Z') idx = c.unicode() - 'A'; + else if (c >= 'a' && c <= 'z') idx = c.unicode() - 'a' + 26; + else if (c >= '0' && c <= '9') idx = c.unicode() - '0' + 52; + else if (c == '+') idx = 62; + else if (c == '/') idx = 63; + wf.append((float)idx / 63.0f); + } item->setData(wf, MsgVoiceWaveformRole); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "setVoiceMessageRoles: voice dur=%dms wf=%d exists=%d", + durMs, wf.size(), QFile::exists(fp)); } } if (ct == "application/octet-stream") { @@ -240,9 +252,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent int durationMs, const QString& wfStr) { if (m_currentChannelId.isEmpty()) return; - QString displayName = basename + "." + ext; - QByteArray textData = (displayName + "|" + - QString::number(durationMs / 1000.0, 'f', 1) + "|" + wfStr).toUtf8(); + QByteArray textData = wfStr.toUtf8(); auto ts = QDateTime::currentMSecsSinceEpoch(); struct chat_msg_submit* req = (struct chat_msg_submit*)