From 6b765de5cc0f46d378c3ebe41cf6c9f51f77da21 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 27 Jul 2026 14:53:53 +0300 Subject: [PATCH] feat: voice playback delay compensation + waveform click-to-seek --- tools/chatgui/src/messagedelegate.cpp | 60 +++++++++++++++++++++++---- tools/chatgui/src/sound_manager.cpp | 6 +++ tools/chatgui/src/sound_manager.h | 3 ++ 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index d6a90f9d..1a2539ac 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -71,15 +71,58 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, QMouseEvent* me = static_cast(event); QPoint localPos = me->pos() - option.rect.topLeft(); - if (!L.voicePlayBtnRect.contains(localPos)) return false; + + bool onPlayBtn = L.voicePlayBtnRect.contains(localPos); + bool onWaveform = L.voiceWaveformRect.contains(localPos); + if (!onPlayBtn && !onWaveform) return false; QString filePath = index.data(MsgVoiceFileRole).toString(); - GUI_DEBUG("editorEvent voice click: filePath=%s exists=%d", - qPrintable(filePath), QFile::exists(filePath)); + GUI_DEBUG("editorEvent voice click: filePath=%s exists=%d wave=%d", + qPrintable(filePath), QFile::exists(filePath), (int)onWaveform); if (filePath.isEmpty()) return false; ensureVoiceTimer(); VoicePlayState& st = s_voiceStates[filePath]; + + QString durStr = index.data(MsgVoiceDurationRole).toString(); + durStr.remove('s'); + float dur = durStr.toFloat(); + if (dur <= 0) dur = 1.0f; + + if (onWaveform) { + float clickFrac = (float)(localPos.x() - L.voiceWaveformRect.x()) / L.voiceWaveformRect.width(); + if (clickFrac < 0) clickFrac = 0; if (clickFrac > 1) clickFrac = 1; + float targetSec = clickFrac * dur; + qint64 now = QDateTime::currentMSecsSinceEpoch(); + + if (!st.playing && !st.paused) { + /* Start playback then seek */ + st.playing = true; + st.paused = false; + st.durationSec = dur; + st.startMs = now - (qint64)(targetSec * 1000.0); + st.pausedPosSec = targetSec; + VoicePlayer::playOpusFile(filePath); + int sr = SoundManager::instance()->currentPcmSampleRate(); + if (sr > 0) + SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); + s_voiceTimer->start(); + model->setData(index, 1, MsgVoicePlayStateRole); + } else { + /* Currently playing or paused — seek */ + st.startMs = now - (qint64)(targetSec * 1000.0); + if (st.paused) + st.pausedPosSec = targetSec; + else if (!st.playing) + { st.playing = true; s_voiceTimer->start(); } + int sr = SoundManager::instance()->currentPcmSampleRate(); + if (sr > 0) + SoundManager::instance()->seekCurrentPcm((unsigned long long)(targetSec * sr)); + } + return true; + } + + /* --- Play button click (original logic) --- */ bool isOtherPlaying = false; for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it) { if (it.key() != filePath && (it->playing || it->paused)) @@ -117,11 +160,6 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, SoundManager::instance()->stopCurrentPcm(); } - QString durStr = index.data(MsgVoiceDurationRole).toString(); - durStr.remove('s'); - float dur = durStr.toFloat(); - if (dur <= 0) dur = 1.0f; - st.playing = true; st.paused = false; st.pausedPosSec = 0; @@ -633,7 +671,11 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio elapsed = (now - st.startMs) / 1000.0f; if (elapsed > durSec) elapsed = durSec; } - if (durSec > 0) frac = 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; QRectF playR = L.voicePlayBtnRect; diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index e98d3dcf..c9458d47 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -153,6 +153,7 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat return; } if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound); + m_currentPcmSampleRate = sampleRate; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SoundManager::playRawPcm: %d samples %dHz %dch", sampleCount, sampleRate, channels); ma_audio_buffer_config bufCfg = ma_audio_buffer_config_init( @@ -189,6 +190,11 @@ void SoundManager::resumeCurrentPcm() { void SoundManager::stopCurrentPcm() { if (m_currentPcmSound) ma_sound_stop(m_currentPcmSound); + m_currentPcmSampleRate = 0; +} + +void SoundManager::seekCurrentPcm(unsigned long long pcmFrame) { + if (m_currentPcmSound) ma_sound_seek_to_pcm_frame(m_currentPcmSound, pcmFrame); } static QString decodeDeviceName(const char* name) { diff --git a/tools/chatgui/src/sound_manager.h b/tools/chatgui/src/sound_manager.h index 54a59290..b4895573 100644 --- a/tools/chatgui/src/sound_manager.h +++ b/tools/chatgui/src/sound_manager.h @@ -42,6 +42,8 @@ public: void pauseCurrentPcm(); void resumeCurrentPcm(); void stopCurrentPcm(); + void seekCurrentPcm(unsigned long long pcmFrame); + int currentPcmSampleRate() const { return m_currentPcmSampleRate; } struct DeviceInfo { QString name; @@ -64,6 +66,7 @@ private: ma_context* m_context = nullptr; ma_sound* m_currentPcmSound = nullptr; void* m_currentPcmBuffer = nullptr; + int m_currentPcmSampleRate = 0; bool m_initialized = false; bool m_enabled = true; float m_volume = 0.8f;