From 26686197582e32a7ee6b01db1137ac0d24e9d3a4 Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Tue, 10 Nov 2020 21:06:22 +0300 Subject: [PATCH] Added ability to seek recorded voice data. --- .../history_view_voice_record_bar.cpp | 155 ++++++++++++++++-- 1 file changed, 142 insertions(+), 13 deletions(-) diff --git a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp index 9e43687b74..5a51b66d59 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_voice_record_bar.cpp @@ -39,6 +39,7 @@ namespace { using SendActionUpdate = VoiceRecordBar::SendActionUpdate; using VoiceToSend = VoiceRecordBar::VoiceToSend; +constexpr auto kAudioVoiceUpdateView = crl::time(200); constexpr auto kLockDelay = crl::time(100); constexpr auto kRecordingUpdateDelta = crl::time(100); constexpr auto kAudioVoiceMaxLength = 100 * 60; // 100 minutes @@ -55,6 +56,10 @@ enum class FilterType { Cancel, }; +[[nodiscard]] auto Progress(int low, int high) { + return std::clamp(float64(low) / high, 0., 1.); +} + [[nodiscard]] auto Duration(int samples) { return samples / ::Media::Player::kDefaultFrequency; } @@ -116,6 +121,10 @@ public: private: void init(); void initPlayButton(); + void initPlayProgress(); + + bool isInPlayer(const ::Media::Player::TrackState &state) const; + bool isInPlayer() const; int computeTopMargin(int height) const; @@ -136,9 +145,12 @@ private: QRect _waveformBgRect; QRect _waveformBgFinalCenterRect; + QRect _waveformFgRect; ::Media::Player::PlayButtonLayout _playPause; + anim::value _playProgress; + rpl::variable _showProgress = 0.; rpl::lifetime _lifetime; @@ -253,13 +265,19 @@ void ListenWrap::init() { // Waveform paint. { - const auto &play = _playPauseSt.playOuter; - const auto top = computeTopMargin(st::msgWaveformMax); - const auto left = play.width() / 2 + halfHeight; - const auto right = st::historyRecordWaveformLeftSkip - + _durationWidth; - const auto rect = bgCenterRect.marginsRemoved( - style::margins(left, top, right, top)); + const auto computeRect = [&] { + const auto &play = _playPauseSt.playOuter; + const auto top = computeTopMargin(st::msgWaveformMax); + const auto left = play.width() / 2 + halfHeight; + const auto right = st::historyRecordWaveformLeftSkip + + _durationWidth; + _waveformFgRect = bgCenterRect.marginsRemoved( + style::margins(left, top, right, top)); + return _waveformFgRect; + }; + const auto rect = (progress == 1.) + ? _waveformFgRect + : computeRect(); if (rect.width() > 0) { p.translate(rect.topLeft()); HistoryDocumentVoice::PaintWaveform( @@ -268,7 +286,7 @@ void ListenWrap::init() { rect.width(), false, false, - 0.); + _playProgress.current()); p.resetTransform(); } } @@ -276,6 +294,7 @@ void ListenWrap::init() { }, _lifetime); initPlayButton(); + initPlayProgress(); } void ListenWrap::initPlayButton() { @@ -317,7 +336,7 @@ void ListenWrap::initPlayButton() { instance()->updatedNotifier( ) | rpl::start_with_next([=](const State &state) { - if (state.id.audio() == _document) { + if (isInPlayer(state)) { *showPause = ShowPauseIcon(state.state); } else if (showPause->current()) { *showPause = false; @@ -332,13 +351,124 @@ void ListenWrap::initPlayButton() { const auto weak = Ui::MakeWeak(_controller->content().get()); _lifetime.add([=] { - const auto voiceState = instance()->getState(AudioMsgId::Type::Voice); - if (weak && voiceState.id && (voiceState.id.audio() == _document)) { + if (weak && isInPlayer()) { weak->stopAndClosePlayer(); } }); } +void ListenWrap::initPlayProgress() { + using namespace ::Media::Player; + using State = TrackState; + + const auto animation = _lifetime.make_state(); + const auto isPointer = _lifetime.make_state>(false); + const auto &voice = AudioMsgId::Type::Voice; + + const auto updateCursor = [=](const QPoint &p) { + *isPointer = isInPlayer() ? _waveformFgRect.contains(p) : false; + }; + + rpl::merge( + instance()->startsPlay(voice) | rpl::map_to(true), + instance()->stops(voice) | rpl::map_to(false) + ) | rpl::start_with_next([=](bool play) { + _parent->setMouseTracking(isInPlayer() && play); + updateCursor(_parent->mapFromGlobal(QCursor::pos())); + }, _lifetime); + + instance()->updatedNotifier( + ) | rpl::start_with_next([=](const State &state) { + if (!isInPlayer(state)) { + return; + } + const auto progress = state.length + ? Progress(state.position, state.length) + : 0.; + if (IsStopped(state.state)) { + _playProgress = anim::value(); + } else { + _playProgress.start(progress); + } + animation->start(); + }, _lifetime); + + auto animationCallback = [=](crl::time now) { + if (anim::Disabled()) { + now += kAudioVoiceUpdateView; + } + + const auto dt = (now - animation->started()) + / float64(kAudioVoiceUpdateView); + if (dt >= 1.) { + animation->stop(); + _playProgress.finish(); + } else { + _playProgress.update(std::min(dt, 1.), anim::linear); + } + _parent->update(); + return (dt < 1.); + }; + animation->init(std::move(animationCallback)); + + const auto isPressed = _lifetime.make_state(false); + + isPointer->changes( + ) | rpl::start_with_next([=](bool pointer) { + _parent->setCursor(pointer ? style::cur_pointer : style::cur_default); + }, _lifetime); + + _parent->events( + ) | rpl::filter([=](not_null e) { + return (e->type() == QEvent::MouseMove + || e->type() == QEvent::MouseButtonPress + || e->type() == QEvent::MouseButtonRelease); + }) | rpl::start_with_next([=](not_null e) { + if (!isInPlayer()) { + return; + } + + const auto type = e->type(); + const auto isMove = (type == QEvent::MouseMove); + const auto &pos = static_cast(e.get())->pos(); + if (*isPressed) { + *isPointer = true; + } else if (isMove) { + updateCursor(pos); + } + if (type == QEvent::MouseButtonPress) { + if (isPointer->current() && !(*isPressed)) { + instance()->startSeeking(voice); + *isPressed = true; + } + } else if (*isPressed) { + const auto &rect = _waveformFgRect; + const auto left = float64(pos.x() - rect.x()); + const auto progress = Progress(left, rect.width()); + const auto isRelease = (type == QEvent::MouseButtonRelease); + if (isRelease || isMove) { + _playProgress = anim::value(progress, progress); + _parent->update(); + if (isRelease) { + instance()->finishSeeking(voice, progress); + *isPressed = false; + } + } + } + + }, _lifetime); +} + + +bool ListenWrap::isInPlayer(const ::Media::Player::TrackState &state) const { + return (state.id && (state.id.audio() == _document)); +} + +bool ListenWrap::isInPlayer() const { + using Type = AudioMsgId::Type; + return isInPlayer(::Media::Player::instance()->getState(Type::Voice)); +} + int ListenWrap::computeTopMargin(int height) const { return (_waveformBgRect.height() - height) / 2; } @@ -1150,8 +1280,7 @@ void VoiceRecordBar::computeAndSetLockProgress(QPoint globalPos) { const auto localPos = mapFromGlobal(globalPos); const auto lower = _lock->height(); const auto higher = 0; - const auto progress = localPos.y() / (float64)(higher - lower); - _lock->requestPaintProgress(std::clamp(progress, 0., 1.)); + _lock->requestPaintProgress(Progress(localPos.y(), higher - lower)); } void VoiceRecordBar::orderControls() {