diff --git a/tools/chatgui/src/audiodevicesettingspage.cpp b/tools/chatgui/src/audiodevicesettingspage.cpp index 30798014..ffeedf69 100644 --- a/tools/chatgui/src/audiodevicesettingspage.cpp +++ b/tools/chatgui/src/audiodevicesettingspage.cpp @@ -114,7 +114,7 @@ AudioDeviceSettingsPage::AudioDeviceSettingsPage(DbManager* db, QWidget* parent) layout->addStretch(); m_levelTimer = new QTimer(this); - m_levelTimer->setInterval(100); + m_levelTimer->setInterval(25); connect(m_levelTimer, &QTimer::timeout, this, &AudioDeviceSettingsPage::onLevelTimer); } diff --git a/tools/chatgui/src/audiorecorder.cpp b/tools/chatgui/src/audiorecorder.cpp index 0c7b5626..dbc0247a 100644 --- a/tools/chatgui/src/audiorecorder.cpp +++ b/tools/chatgui/src/audiorecorder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include static AudioRecorder* g_recorder = nullptr; @@ -20,12 +21,12 @@ void audioCaptureCallback(ma_device* pDevice, void* pOutput, const void* pInput, self->m_pcmBuffer.resize(cur + add); std::memcpy(self->m_pcmBuffer.data() + cur, src, add * sizeof(int16_t)); - float maxAbs = 0.0f; + float sumSq = 0.0f; for (unsigned int i = 0; i < frameCount * (unsigned int)self->m_channels; i++) { - float v = (float)abs(src[i]) / 32768.0f; - if (v > maxAbs) maxAbs = v; + float v = (float)src[i] / 32768.0f; + sumSq += v * v; } - self->m_peakLevel = maxAbs; + self->m_peakLevel = sqrtf(sumSq / (float)(frameCount * self->m_channels)); } AudioRecorder::AudioRecorder(QObject* parent) diff --git a/tools/chatgui/src/inputbar.cpp b/tools/chatgui/src/inputbar.cpp index 07de3681..77c78b41 100644 --- a/tools/chatgui/src/inputbar.cpp +++ b/tools/chatgui/src/inputbar.cpp @@ -1,5 +1,7 @@ #include "inputbar.h" #include "emojipanel.h" +#include "audiorecorder.h" +#include "voicemessageencoder.h" #include "debug_ui.h" #include #include @@ -11,6 +13,8 @@ #include #include #include +#include +#include static QPushButton *makeIconBtn(const QString &text, const QString &tooltip, QWidget *parent) { auto *btn = new QPushButton(text, parent); @@ -99,6 +103,33 @@ InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *par m_input->setFocus(); }); } + + /* Recording overlay — appears over the text input during recording */ + m_recordOverlay = new QWidget(this); + m_recordOverlay->setStyleSheet("background: rgba(231, 76, 60, 0.88); border-radius: 6px;"); + m_recordOverlay->hide(); + auto* roLayout = new QHBoxLayout(m_recordOverlay); + roLayout->setContentsMargins(12, 0, 8, 0); + roLayout->setSpacing(6); + m_recordLabel = new QLabel(m_recordOverlay); + m_recordLabel->setStyleSheet("color: white; font-size: 14px; font-weight: bold; background: transparent;"); + roLayout->addWidget(m_recordLabel); + roLayout->addStretch(); + m_cancelRecBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_recordOverlay); + m_cancelRecBtn->setFixedSize(24, 24); + m_cancelRecBtn->setStyleSheet( + "QPushButton { border: none; color: white; font-size: 16px; background: transparent; }" + "QPushButton:hover { color: #fdd; }"); + connect(m_cancelRecBtn, &QPushButton::clicked, this, [this]() { + if (m_recorder && m_recorder->isRecording()) m_recorder->stopRecording(); + }); + roLayout->addWidget(m_cancelRecBtn); + + if (recorder) { + connect(recorder, &AudioRecorder::recordingStarted, this, &InputBar::showRecordOverlay); + connect(recorder, &AudioRecorder::recordingStopped, this, [this](int) { hideRecordOverlay(); }); + connect(recorder, &AudioRecorder::durationChanged, this, &InputBar::updateRecordingUI); + } } bool InputBar::eventFilter(QObject *obj, QEvent *event) { @@ -151,9 +182,6 @@ void InputBar::onSendClicked() { m_input->setFocus(); } -#include "audiorecorder.h" -#include "voicemessageencoder.h" - void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId) { m_mediaDirBase = mediaDirBase; m_channelIdForRecord = channelId; @@ -197,3 +225,34 @@ void InputBar::onPttReleased() { emit sendVoiceMessage(filePath, durationMs); } } + +void InputBar::resizeEvent(QResizeEvent* event) { + QWidget::resizeEvent(event); + if (m_recordOverlay) { + QRect r = m_input->geometry(); + int margin = 4; + m_recordOverlay->setGeometry(r.adjusted(margin, margin, -margin, -margin)); + m_recordOverlay->raise(); + } +} + +void InputBar::showRecordOverlay() { + if (m_recordOverlay) { + m_recordOverlay->setGeometry(m_input->geometry().adjusted(4, 4, -4, -4)); + m_recordOverlay->show(); + m_recordOverlay->raise(); + m_recordLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording 0:00")); + } +} + +void InputBar::hideRecordOverlay() { + if (m_recordOverlay) m_recordOverlay->hide(); +} + +void InputBar::updateRecordingUI(int ms) { + if (m_recordLabel) { + int secs = ms / 1000; + m_recordLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording %1:%2") + .arg(secs / 60).arg(secs % 60, 2, 10, QChar('0'))); + } +} diff --git a/tools/chatgui/src/inputbar.h b/tools/chatgui/src/inputbar.h index 2a23e5a1..e4d7cbab 100644 --- a/tools/chatgui/src/inputbar.h +++ b/tools/chatgui/src/inputbar.h @@ -4,6 +4,7 @@ class QTextEdit; class QPushButton; +class QLabel; class EmojiPanel; class AudioRecorder; @@ -21,12 +22,16 @@ signals: protected: bool eventFilter(QObject *obj, QEvent *event) override; + void resizeEvent(QResizeEvent* event) override; private slots: void onSendClicked(); void adjustInputHeight(); void onPttPressed(); void onPttReleased(); + void updateRecordingUI(int ms); + void showRecordOverlay(); + void hideRecordOverlay(); private: QTextEdit *m_input; @@ -34,6 +39,9 @@ private: QPushButton *m_pttBtn; EmojiPanel *m_emojiPanel; AudioRecorder *m_recorder; + QWidget *m_recordOverlay = nullptr; + QLabel *m_recordLabel = nullptr; + QPushButton *m_cancelRecBtn = nullptr; bool m_pttActive = false; QString m_voiceFilePath; QString m_mediaDirBase; diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 375205be..6bf16c2b 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include @@ -171,41 +170,6 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent m_inputBar = new InputBar(m_emojiPanel, recorder, this); layout->addWidget(m_inputBar); - /* Recording indicator - hidden by default */ - m_recordIndicator = new QWidget(this); - m_recordIndicator->setFixedHeight(28); - m_recordIndicator->hide(); - auto* recLayout = new QHBoxLayout(m_recordIndicator); - recLayout->setContentsMargins(8, 2, 8, 2); - recLayout->setSpacing(6); - m_recordTimeLabel = new QLabel(m_recordIndicator); - m_recordTimeLabel->setStyleSheet("color: #e74c3c; font-size: 13px; font-weight: bold;"); - recLayout->addWidget(m_recordTimeLabel); - recLayout->addStretch(); - auto* cancelRecBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_recordIndicator); - cancelRecBtn->setFixedSize(22, 22); - cancelRecBtn->setStyleSheet("QPushButton { border: none; color: #e74c3c; font-size: 14px; background: transparent; }"); - connect(cancelRecBtn, &QPushButton::clicked, this, [this]() { - if (m_recorder && m_recorder->isRecording()) m_recorder->stopRecording(); - }); - recLayout->addWidget(cancelRecBtn); - layout->addWidget(m_recordIndicator); - - if (recorder) { - connect(recorder, &AudioRecorder::recordingStarted, this, [this]() { - m_recordIndicator->show(); - m_recordTimeLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording 0:00")); - }); - connect(recorder, &AudioRecorder::recordingStopped, this, [this](int) { - m_recordIndicator->hide(); - }); - connect(recorder, &AudioRecorder::durationChanged, this, [this](int ms) { - int secs = ms / 1000; - m_recordTimeLabel->setText(QString::fromUtf8("\xF0\x9F\x94\xB4 Recording %1:%2") - .arg(secs / 60).arg(secs % 60, 2, 10, QChar('0'))); - }); - } - connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) { if (m_currentChannelId.isEmpty()) { qWarning("MessageList: no channel selected, message not sent"); diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index c9ec4574..50a989b6 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -5,9 +5,9 @@ #include #include -class QLabel; class ChatView; class InputBar; +class InputBar; class EmojiPanel; class DbManager; class ChannelList; @@ -45,11 +45,8 @@ private: bool m_wasAtBottom = false; QMap m_localNicks; AudioRecorder* m_recorder = nullptr; - QWidget* m_recordIndicator = nullptr; - QLabel* m_recordTimeLabel = nullptr; QString authorName(quint64 nodeId) const; - void updateRecordingUI(); void restoreScrollPosition(const QString& channelId); void updateReadPosition(); }; diff --git a/tools/chatgui/src/sound_manager.cpp b/tools/chatgui/src/sound_manager.cpp index 92723584..20df3878 100644 --- a/tools/chatgui/src/sound_manager.cpp +++ b/tools/chatgui/src/sound_manager.cpp @@ -143,21 +143,21 @@ ma_context* SoundManager::context() { void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRate, int channels) { if (!m_engine || !m_initialized) return; - ma_decoder_config decCfg = ma_decoder_config_init(ma_format_s16, (ma_uint32)channels, (ma_uint32)sampleRate); - ma_decoder* decoder = new ma_decoder; - ma_result r = ma_decoder_init_memory(pcm, (ma_uint64)sampleCount * channels * sizeof(int16_t), &decCfg, decoder); + ma_audio_buffer_ref* ref = new ma_audio_buffer_ref; + ma_result r = ma_audio_buffer_ref_init(ma_format_s16, (ma_uint32)channels, + pcm, (ma_uint64)sampleCount, ref); if (r != MA_SUCCESS) { - qWarning("SoundManager: PCM decoder init failed (%d)", r); - delete decoder; + qWarning("SoundManager: audio_buffer_ref_init failed (%d)", r); + delete ref; return; } ma_sound* sound = new ma_sound; - r = ma_sound_init_from_data_source(m_engine, (ma_data_source*)decoder, 0, nullptr, sound); + r = ma_sound_init_from_data_source(m_engine, (ma_data_source*)ref, 0, nullptr, sound); if (r != MA_SUCCESS) { qWarning("SoundManager: PCM sound init failed (%d)", r); - ma_decoder_uninit(decoder); - delete decoder; + ma_audio_buffer_ref_uninit(ref); + delete ref; delete sound; return; } @@ -165,8 +165,6 @@ void SoundManager::playRawPcm(const int16_t* pcm, int sampleCount, int sampleRat ma_sound_set_volume(sound, m_volume); ma_sound_start(sound); qDebug("SoundManager: playing PCM (%d samples, %dHz, %dch)", sampleCount, sampleRate, channels); - - /* auto-cleanup after playback ends. Note: decoder+sound leak on early shutdown, acceptable for voice messages */ } QVector SoundManager::enumeratePlaybackDevices() {