From 23874a0a26a86ddd51856bfadef3dd89e5a32214 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 7 May 2017 22:09:20 +0300 Subject: [PATCH] Animate call answer button with an outer ripple. --- Telegram/Resources/colors.palette | 1 + Telegram/SourceFiles/calls/calls.style | 16 ++-- Telegram/SourceFiles/calls/calls_call.cpp | 10 +++ Telegram/SourceFiles/calls/calls_call.h | 2 + Telegram/SourceFiles/calls/calls_panel.cpp | 84 +++++++++++++++---- Telegram/SourceFiles/calls/calls_panel.h | 1 + Telegram/SourceFiles/media/media_audio.cpp | 45 ++++------ Telegram/SourceFiles/media/media_audio.h | 27 ++++++ .../SourceFiles/media/media_audio_track.cpp | 51 ++++++++++- .../SourceFiles/media/media_audio_track.h | 10 +++ Telegram/SourceFiles/ui/widgets/widgets.style | 2 + 11 files changed, 190 insertions(+), 59 deletions(-) diff --git a/Telegram/Resources/colors.palette b/Telegram/Resources/colors.palette index fb1a421a3..662783bad 100644 --- a/Telegram/Resources/colors.palette +++ b/Telegram/Resources/colors.palette @@ -528,6 +528,7 @@ callStatusFg: #aaabac; // phone call popup status text callIconFg: #ffffff; // phone call popup answer, hangup and mute mic icon callAnswerBg: #64c15b; // phone call popup answer button background callAnswerRipple: #52b149; // phone call popup answer button ripple effect +callAnswerBgOuter: #50eb4126; // phone call popup answer button outer ripple effect callHangupBg: #d75a5a; // phone call popup hangup button background callHangupRipple: #c04646; // phone call popup hangup button ripple effect callCancelBg: #ffffff; // phone call popup line busy cancel button background diff --git a/Telegram/SourceFiles/calls/calls.style b/Telegram/SourceFiles/calls/calls.style index 379b761ab..7488f4174 100644 --- a/Telegram/SourceFiles/calls/calls.style +++ b/Telegram/SourceFiles/calls/calls.style @@ -39,12 +39,12 @@ callShadow: Shadow { } callButton: IconButton { - width: 64px; - height: 64px; + width: 72px; + height: 72px; iconPosition: point(-1px, -1px); - rippleAreaPosition: point(8px, 8px); + rippleAreaPosition: point(12px, 12px); rippleAreaSize: 48px; ripple: defaultRippleAnimation; } @@ -58,6 +58,8 @@ callAnswer: CallButton { } bg: callAnswerBg; angle: 135.; + outerRadius: 12px; + outerBg: callAnswerBgOuter; } callHangup: CallButton { button: IconButton(callButton) { @@ -67,6 +69,7 @@ callHangup: CallButton { } } bg: callHangupBg; + outerBg: callHangupBg; } callCancel: CallButton { button: IconButton(callButton) { @@ -76,6 +79,7 @@ callCancel: CallButton { } } bg: callCancelBg; + outerBg: callCancelBg; } callMuteToggle: IconButton(callButton) { icon: icon {{ "call_record_active", callIconFg }}; @@ -85,9 +89,9 @@ callMuteToggle: IconButton(callButton) { } callUnmuteIcon: icon {{ "call_record_muted", callIconFg }}; -callControlsTop: 84px; -callControlsSkip: 8px; -callMuteRight: 12px; +callControlsTop: 80px; +callControlsSkip: 0px; +callMuteRight: 8px; callNameTop: 15px; callName: FlatLabel(defaultFlatLabel) { diff --git a/Telegram/SourceFiles/calls/calls_call.cpp b/Telegram/SourceFiles/calls/calls_call.cpp index e270768f6..deb2ed2a4 100644 --- a/Telegram/SourceFiles/calls/calls_call.cpp +++ b/Telegram/SourceFiles/calls/calls_call.cpp @@ -29,6 +29,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org #include "base/openssl_help.h" #include "mtproto/connection.h" #include "media/media_audio_track.h" +#include "calls/calls_panel.h" #ifdef slots #undef slots @@ -237,10 +238,19 @@ QString Call::getDebugLog() const { void Call::startWaitingTrack() { _waitingTrack = Media::Audio::Current().createTrack(); auto trackFileName = (_type == Type::Outgoing) ? qsl(":/sounds/call_outgoing.mp3") : qsl(":/sounds/call_incoming.mp3"); + _waitingTrack->samplePeakEach(kSoundSampleMs); _waitingTrack->fillFromFile(trackFileName); _waitingTrack->playInLoop(); } +float64 Call::getWaitingSoundPeakValue() const { + if (_waitingTrack) { + auto when = getms() + kSoundSampleMs / 4; + return _waitingTrack->getPeakValue(when); + } + return 0.; +} + bool Call::isKeyShaForFingerprintReady() const { return (_keyFingerprint != 0); } diff --git a/Telegram/SourceFiles/calls/calls_call.h b/Telegram/SourceFiles/calls/calls_call.h index e9999cba4..eab2b4943 100644 --- a/Telegram/SourceFiles/calls/calls_call.h +++ b/Telegram/SourceFiles/calls/calls_call.h @@ -63,6 +63,7 @@ public: static constexpr auto kRandomPowerSize = 256; static constexpr auto kSha256Size = 32; + static constexpr auto kSoundSampleMs = 100; enum class Type { Incoming, @@ -111,6 +112,7 @@ public: } TimeMs getDurationMs() const; + float64 getWaitingSoundPeakValue() const; void answer(); void hangup(); diff --git a/Telegram/SourceFiles/calls/calls_panel.cpp b/Telegram/SourceFiles/calls/calls_panel.cpp index b46f320b9..159617ca2 100644 --- a/Telegram/SourceFiles/calls/calls_panel.cpp +++ b/Telegram/SourceFiles/calls/calls_panel.cpp @@ -48,6 +48,7 @@ public: Button(QWidget *parent, const style::CallButton &stFrom, const style::CallButton *stTo = nullptr); void setProgress(float64 progress); + void setOuterValue(float64 value); protected: void paintEvent(QPaintEvent *e) override; @@ -69,6 +70,9 @@ private: QPixmap _bgFrom, _bgTo; QImage _iconMixedMask, _iconFrom, _iconTo, _iconMixed; + float64 _outerValue = 0.; + Animation _outerAnimation; + }; Panel::Button::Button(QWidget *parent, const style::CallButton &stFrom, const style::CallButton *stTo) : Ui::RippleButton(parent, stFrom.button.ripple) @@ -108,6 +112,17 @@ Panel::Button::Button(QWidget *parent, const style::CallButton &stFrom, const st } } +void Panel::Button::setOuterValue(float64 value) { + if (_outerValue != value) { + _outerAnimation.start([this] { + if (_progress == 0. || _progress == 1.) { + update(); + } + }, _outerValue, value, Call::kSoundSampleMs); + _outerValue = value; + } +} + void Panel::Button::setProgress(float64 progress) { _progress = progress; update(); @@ -116,9 +131,30 @@ void Panel::Button::setProgress(float64 progress) { void Panel::Button::paintEvent(QPaintEvent *e) { Painter p(this); + auto ms = getms(); + auto bgPosition = myrtlpoint(_stFrom->button.rippleAreaPosition); auto paintFrom = (_progress == 0.) || !_stTo; auto paintTo = !paintFrom && (_progress == 1.); - auto bgPosition = myrtlpoint(_stFrom->button.rippleAreaPosition); + + auto outerValue = _outerAnimation.current(ms, _outerValue); + if (outerValue > 0.) { + auto outerRadius = paintFrom ? _stFrom->outerRadius : paintTo ? _stTo->outerRadius : (_stFrom->outerRadius * (1. - _progress) + _stTo->outerRadius * _progress); + auto outerPixels = outerValue * outerRadius; + auto outerRect = QRectF(myrtlrect(bgPosition.x(), bgPosition.y(), _stFrom->button.rippleAreaSize, _stFrom->button.rippleAreaSize)); + outerRect = outerRect.marginsAdded(QMarginsF(outerPixels, outerPixels, outerPixels, outerPixels)); + + PainterHighQualityEnabler hq(p); + if (paintFrom) { + p.setBrush(_stFrom->outerBg); + } else if (paintTo) { + p.setBrush(_stTo->outerBg); + } else { + p.setBrush(anim::brush(_stFrom->outerBg, _stTo->outerBg, _progress)); + } + p.setPen(Qt::NoPen); + p.drawEllipse(outerRect); + } + if (paintFrom) { p.drawPixmap(bgPosition, _bgFrom); } else if (paintTo) { @@ -128,8 +164,6 @@ void Panel::Button::paintEvent(QPaintEvent *e) { p.drawImage(bgPosition, _bg); } - auto ms = getms(); - auto rippleColorInterpolated = QColor(); auto rippleColorOverride = &rippleColorInterpolated; if (paintFrom) { @@ -269,6 +303,14 @@ void Panel::initControls() { updateStatusText(_call->state()); } }); + _updateOuterRippleTimer.setCallback([this] { + if (_call) { + _answerHangupRedial->setOuterValue(_call->getWaitingSoundPeakValue()); + } else { + _answerHangupRedial->setOuterValue(0.); + _updateOuterRippleTimer.cancel(); + } + }); _answerHangupRedial->setClickedCallback([this] { if (!_call || _hangupShownProgress.animating()) { return; @@ -652,22 +694,28 @@ void Panel::stateChanged(State state) { updateStatusText(state); if (_call) { - auto toggleButton = [this](auto &&button, bool visible) { - if (isHidden()) { - button->toggleFast(visible); - } else { - button->toggleAnimated(visible); + if ((state != State::HangingUp) && (state != State::Ended) && (state != State::Failed)) { + auto toggleButton = [this](auto &&button, bool visible) { + if (isHidden()) { + button->toggleFast(visible); + } else { + button->toggleAnimated(visible); + } + }; + auto waitingIncoming = (_call->type() == Call::Type::Incoming) && ((state == State::Starting) || (state == State::WaitingIncoming)); + if (waitingIncoming) { + _updateOuterRippleTimer.callEach(Call::kSoundSampleMs); + } + toggleButton(_decline, waitingIncoming); + toggleButton(_cancel, (state == State::Busy)); + auto hangupShown = _decline->isHiddenOrHiding() && _cancel->isHiddenOrHiding(); + if (_hangupShown != hangupShown) { + _hangupShown = hangupShown; + _hangupShownProgress.start([this] { updateHangupGeometry(); }, _hangupShown ? 0. : 1., _hangupShown ? 1. : 0., st::callPanelDuration, anim::sineInOut); + } + if (_fingerprint.empty() && _call->isKeyShaForFingerprintReady()) { + fillFingerprint(); } - }; - toggleButton(_decline, (_call->type() == Call::Type::Incoming) && ((state == State::Starting) || (state == State::WaitingIncoming))); - toggleButton(_cancel, (state == State::Busy)); - auto hangupShown = _decline->isHiddenOrHiding() && _cancel->isHiddenOrHiding(); - if (_hangupShown != hangupShown) { - _hangupShown = hangupShown; - _hangupShownProgress.start([this] { updateHangupGeometry(); }, _hangupShown ? 0. : 1., _hangupShown ? 1. : 0., st::callPanelDuration, anim::sineInOut); - } - if (_fingerprint.empty() && _call->isKeyShaForFingerprintReady()) { - fillFingerprint(); } } diff --git a/Telegram/SourceFiles/calls/calls_panel.h b/Telegram/SourceFiles/calls/calls_panel.h index 8c097203d..ac99b7b11 100644 --- a/Telegram/SourceFiles/calls/calls_panel.h +++ b/Telegram/SourceFiles/calls/calls_panel.h @@ -113,6 +113,7 @@ private: QRect _fingerprintArea; base::Timer _updateDurationTimer; + base::Timer _updateOuterRippleTimer; bool _visible = false; QPixmap _userPhoto; diff --git a/Telegram/SourceFiles/media/media_audio.cpp b/Telegram/SourceFiles/media/media_audio.cpp index e943c729a..c3f93039d 100644 --- a/Telegram/SourceFiles/media/media_audio.cpp +++ b/Telegram/SourceFiles/media/media_audio.cpp @@ -1552,8 +1552,17 @@ public: QVector peaks; peaks.reserve(Media::Player::kWaveformSamplesCount); - int32 fmt = format(); - uint16 peak = 0; + auto fmt = format(); + auto peak = uint16(0); + auto callback = [&peak, &sumbytes, &peaks, countbytes](uint16 sample) { + accumulate_max(peak, sample); + sumbytes += Media::Player::kWaveformSamplesCount; + if (sumbytes >= countbytes) { + sumbytes -= countbytes; + peaks.push_back(peak); + peak = 0; + } + }; while (processed < countbytes) { buffer.resize(0); @@ -1566,37 +1575,11 @@ public: continue; } - const char *data = buffer.data(); + auto sampleBytes = gsl::as_bytes(gsl::make_span(buffer)); if (fmt == AL_FORMAT_MONO8 || fmt == AL_FORMAT_STEREO8) { - for (int32 i = 0, l = buffer.size(); i + int32(sizeof(uchar)) <= l;) { - uint16 sample = qAbs((int32(*(uchar*)(data + i)) - 128) * 256); - if (peak < sample) { - peak = sample; - } - - i += sizeof(uchar); - sumbytes += Media::Player::kWaveformSamplesCount; - if (sumbytes >= countbytes) { - sumbytes -= countbytes; - peaks.push_back(peak); - peak = 0; - } - } + Media::Audio::IterateSamples(sampleBytes, callback); } else if (fmt == AL_FORMAT_MONO16 || fmt == AL_FORMAT_STEREO16) { - for (int32 i = 0, l = buffer.size(); i + int32(sizeof(uint16)) <= l;) { - uint16 sample = qAbs(int32(*(int16*)(data + i))); - if (peak < sample) { - peak = sample; - } - - i += sizeof(uint16); - sumbytes += sizeof(uint16) * Media::Player::kWaveformSamplesCount; - if (sumbytes >= countbytes) { - sumbytes -= countbytes; - peaks.push_back(peak); - peak = 0; - } - } + Media::Audio::IterateSamples(sampleBytes, callback); } processed += sampleSize * samples; } diff --git a/Telegram/SourceFiles/media/media_audio.h b/Telegram/SourceFiles/media/media_audio.h index 69e325479..928797ac1 100644 --- a/Telegram/SourceFiles/media/media_audio.h +++ b/Telegram/SourceFiles/media/media_audio.h @@ -43,6 +43,9 @@ void ScheduleDetachFromDeviceSafe(); void ScheduleDetachIfNotUsedSafe(); void StopDetachIfNotUsedSafe(); +template +void IterateSamples(); + } // namespace Audio namespace Player { @@ -317,3 +320,27 @@ bool audioCheckError(); } // namespace Media VoiceWaveform audioCountWaveform(const FileLocation &file, const QByteArray &data); + +namespace Media { +namespace Audio { + +FORCE_INLINE uint16 ReadOneSample(uchar data) { + return qAbs((static_cast(data) - 0x80) * 0x100); +} + +FORCE_INLINE uint16 ReadOneSample(int16 data) { + return qAbs(data); +} + +template +void IterateSamples(base::const_byte_span bytes, Callback &&callback) { + auto samplesPointer = reinterpret_cast(bytes.data()); + auto samplesCount = bytes.size() / sizeof(SampleType); + auto samplesData = gsl::make_span(samplesPointer, samplesCount); + for (auto sampleData : samplesData) { + callback(ReadOneSample(sampleData)); + } +} + +} // namespace Audio +} // namespace Media diff --git a/Telegram/SourceFiles/media/media_audio_track.cpp b/Telegram/SourceFiles/media/media_audio_track.cpp index d08563029..092d07409 100644 --- a/Telegram/SourceFiles/media/media_audio_track.cpp +++ b/Telegram/SourceFiles/media/media_audio_track.cpp @@ -58,6 +58,10 @@ Track::Track(gsl::not_null instance) : _instance(instance) { _instance->registerTrack(this); } +void Track::samplePeakEach(TimeMs peakDuration) { + _peakDurationMs = peakDuration; +} + void Track::fillFromData(base::byte_vector &&data) { FFMpegLoader loader(FileLocation(), QByteArray(), std::move(data)); @@ -66,15 +70,40 @@ void Track::fillFromData(base::byte_vector &&data) { _failed = true; return; } - + auto format = loader.format(); + _peakEachPosition = _peakDurationMs ? ((loader.samplesFrequency() * _peakDurationMs) / 1000) : 0; + auto peaksCount = _peakEachPosition ? (loader.samplesCount() / _peakEachPosition) : 0; + _peaks.reserve(peaksCount); + auto peakValue = uint16(0); + auto peakSamples = 0; + auto peakEachSample = (format == AL_FORMAT_STEREO8 || format == AL_FORMAT_STEREO16) ? (_peakEachPosition * 2) : _peakEachPosition; + _peakValueMin = 0x7FFF; + _peakValueMax = 0; + auto peakCallback = [this, &peakValue, &peakSamples, peakEachSample](uint16 sample) { + accumulate_max(peakValue, sample); + if (++peakSamples >= peakEachSample) { + peakSamples -= peakEachSample; + _peaks.push_back(peakValue); + accumulate_max(_peakValueMax, peakValue); + accumulate_min(_peakValueMin, peakValue); + peakValue = 0; + } + }; do { auto buffer = QByteArray(); - int64 samplesAdded = 0; + auto samplesAdded = int64(0); auto result = loader.readMore(buffer, samplesAdded); if (samplesAdded > 0) { - auto bufferBytes = reinterpret_cast(buffer.constData()); + auto sampleBytes = gsl::as_bytes(gsl::make_span(buffer)); _samplesCount += samplesAdded; - _samples.insert(_samples.end(), bufferBytes, bufferBytes + buffer.size()); + _samples.insert(_samples.end(), sampleBytes.data(), sampleBytes.data() + sampleBytes.size()); + if (peaksCount) { + if (format == AL_FORMAT_MONO8 || format == AL_FORMAT_STEREO8) { + Media::Audio::IterateSamples(sampleBytes, peakCallback); + } else if (format == AL_FORMAT_MONO16 || format == AL_FORMAT_STEREO16) { + Media::Audio::IterateSamples(sampleBytes, peakCallback); + } + } } using Result = AudioPlayerLoader::ReadResult; @@ -175,6 +204,7 @@ void Track::updateState() { return; } + _stateUpdatedAt = getms(); auto state = ALint(0); alGetSourcei(_alSource, AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { @@ -186,6 +216,19 @@ void Track::updateState() { } } +float64 Track::getPeakValue(TimeMs when) const { + if (!isActive() || !_samplesCount || _peaks.empty() || _peakValueMin == _peakValueMax) { + return 0.; + } + auto sampleIndex = (_alPosition + ((when - _stateUpdatedAt) * _sampleRate / 1000)); + while (sampleIndex < 0) { + sampleIndex += _samplesCount; + } + sampleIndex = sampleIndex % _samplesCount; + auto peakIndex = (sampleIndex / _peakEachPosition) % _peaks.size(); + return (_peaks[peakIndex] - _peakValueMin) / float64(_peakValueMax - _peakValueMin); +} + void Track::detachFromDevice() { if (alIsSource(_alSource)) { updateState(); diff --git a/Telegram/SourceFiles/media/media_audio_track.h b/Telegram/SourceFiles/media/media_audio_track.h index 6e17b9a88..84f2339a9 100644 --- a/Telegram/SourceFiles/media/media_audio_track.h +++ b/Telegram/SourceFiles/media/media_audio_track.h @@ -31,6 +31,8 @@ class Track { public: Track(gsl::not_null instance); + void samplePeakEach(TimeMs peakDuration); + void fillFromData(base::byte_vector &&data); void fillFromFile(const FileLocation &location); void fillFromFile(const QString &filePath); @@ -55,6 +57,7 @@ public: int64 getLengthMs() const { return _lengthMs; } + float64 getPeakValue(TimeMs when) const; void detachFromDevice(); void reattachToDevice(); @@ -78,7 +81,14 @@ private: int32 _sampleRate = 0; base::byte_vector _samples; + TimeMs _peakDurationMs = 0; + int _peakEachPosition = 0; + std::vector _peaks; + uint16 _peakValueMin = 0; + uint16 _peakValueMax = 0; + TimeMs _lengthMs = 0; + TimeMs _stateUpdatedAt = 0; int32 _alFormat = 0; int64 _alPosition = 0; diff --git a/Telegram/SourceFiles/ui/widgets/widgets.style b/Telegram/SourceFiles/ui/widgets/widgets.style index 6ef2fb3d9..637d013ac 100644 --- a/Telegram/SourceFiles/ui/widgets/widgets.style +++ b/Telegram/SourceFiles/ui/widgets/widgets.style @@ -356,6 +356,8 @@ CallButton { button: IconButton; bg: color; angle: double; + outerRadius: pixels; + outerBg: color; } Menu {