Added initial ability to pause and resume voice recording.

This commit is contained in:
23rd 2024-01-24 04:38:49 +03:00 committed by John Preston
parent 5130c5df80
commit 091c13bc23
6 changed files with 215 additions and 124 deletions

View file

@ -1019,6 +1019,7 @@ PRIVATE
media/audio/media_audio.h media/audio/media_audio.h
media/audio/media_audio_capture.cpp media/audio/media_audio_capture.cpp
media/audio/media_audio_capture.h media/audio/media_audio_capture.h
media/audio/media_audio_capture_common.h
media/audio/media_audio_ffmpeg_loader.cpp media/audio/media_audio_ffmpeg_loader.cpp
media/audio/media_audio_ffmpeg_loader.h media/audio/media_audio_ffmpeg_loader.h
media/audio/media_audio_loader.cpp media/audio/media_audio_loader.cpp

View file

@ -84,16 +84,14 @@ enum class FilterType {
const int duration = kPrecision const int duration = kPrecision
* (float64(samples) / ::Media::Player::kDefaultFrequency); * (float64(samples) / ::Media::Player::kDefaultFrequency);
const auto durationString = Ui::FormatDurationText(duration / kPrecision); const auto durationString = Ui::FormatDurationText(duration / kPrecision);
const auto decimalPart = duration % kPrecision; const auto decimalPart = QString::number(duration % kPrecision);
return QString("%1%2%3") return durationString + QLocale().decimalPoint() + decimalPart;
.arg(durationString, QLocale().decimalPoint())
.arg(decimalPart);
} }
[[nodiscard]] std::unique_ptr<VoiceData> ProcessCaptureResult( [[nodiscard]] std::unique_ptr<VoiceData> ProcessCaptureResult(
const ::Media::Capture::Result &data) { const VoiceWaveform &waveform) {
auto voiceData = std::make_unique<VoiceData>(); auto voiceData = std::make_unique<VoiceData>();
voiceData->waveform = data.waveform; voiceData->waveform = waveform;
voiceData->wavemax = voiceData->waveform.empty() voiceData->wavemax = voiceData->waveform.empty()
? uchar(0) ? uchar(0)
: *ranges::max_element(voiceData->waveform); : *ranges::max_element(voiceData->waveform);
@ -427,12 +425,11 @@ public:
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::RecordBar &st, const style::RecordBar &st,
not_null<Main::Session*> session, not_null<Main::Session*> session,
::Media::Capture::Result &&data, ::Media::Capture::Result *data,
const style::font &font); const style::font &font);
void requestPaintProgress(float64 progress); void requestPaintProgress(float64 progress);
rpl::producer<> stopRequests() const; rpl::producer<> stopRequests() const;
::Media::Capture::Result *data() const;
void playPause(); void playPause();
@ -456,7 +453,7 @@ private:
const not_null<DocumentData*> _document; const not_null<DocumentData*> _document;
const std::unique_ptr<VoiceData> _voiceData; const std::unique_ptr<VoiceData> _voiceData;
const std::shared_ptr<Data::DocumentMedia> _mediaView; const std::shared_ptr<Data::DocumentMedia> _mediaView;
const std::unique_ptr<::Media::Capture::Result> _data; const not_null<::Media::Capture::Result*> _data;
const base::unique_qptr<Ui::IconButton> _delete; const base::unique_qptr<Ui::IconButton> _delete;
const style::font &_durationFont; const style::font &_durationFont;
const QString _duration; const QString _duration;
@ -486,15 +483,15 @@ ListenWrap::ListenWrap(
not_null<Ui::RpWidget*> parent, not_null<Ui::RpWidget*> parent,
const style::RecordBar &st, const style::RecordBar &st,
not_null<Main::Session*> session, not_null<Main::Session*> session,
::Media::Capture::Result &&data, ::Media::Capture::Result *data,
const style::font &font) const style::font &font)
: _parent(parent) : _parent(parent)
, _st(st) , _st(st)
, _session(session) , _session(session)
, _document(DummyDocument(&session->data())) , _document(DummyDocument(&session->data()))
, _voiceData(ProcessCaptureResult(data)) , _voiceData(ProcessCaptureResult(data->waveform))
, _mediaView(_document->createMediaView()) , _mediaView(_document->createMediaView())
, _data(std::make_unique<::Media::Capture::Result>(std::move(data))) , _data(data)
, _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove)) , _delete(base::make_unique_q<Ui::IconButton>(parent, _st.remove))
, _durationFont(font) , _durationFont(font)
, _duration(Ui::FormatDurationText( , _duration(Ui::FormatDurationText(
@ -817,10 +814,6 @@ rpl::producer<> ListenWrap::stopRequests() const {
return _delete->clicks() | rpl::to_empty; return _delete->clicks() | rpl::to_empty;
} }
::Media::Capture::Result *ListenWrap::data() const {
return _data.get();
}
rpl::lifetime &ListenWrap::lifetime() { rpl::lifetime &ListenWrap::lifetime() {
return _lifetime; return _lifetime;
} }
@ -1293,12 +1286,14 @@ void VoiceRecordBar::updateTTLGeometry(
const auto from = -_ttlButton->width(); const auto from = -_ttlButton->width();
const auto right = anim::interpolate(from, finalRight, progress); const auto right = anim::interpolate(from, finalRight, progress);
_ttlButton->moveToRight(right, _ttlButton->y()); _ttlButton->moveToRight(right, _ttlButton->y());
#if 0
} else if (type == TTLAnimationType::TopBottom) { } else if (type == TTLAnimationType::TopBottom) {
const auto ttlFrom = anyTop - _ttlButton->height() * 2; const auto ttlFrom = anyTop - _ttlButton->height() * 2;
const auto ttlTo = anyTop - _lock->height(); const auto ttlTo = anyTop - _lock->height();
_ttlButton->moveToLeft( _ttlButton->moveToLeft(
_ttlButton->x(), _ttlButton->x(),
anim::interpolate(ttlFrom, ttlTo, 1. - progress)); anim::interpolate(ttlFrom, ttlTo, 1. - progress));
#endif
} else if (type == TTLAnimationType::RightTopStatic) { } else if (type == TTLAnimationType::RightTopStatic) {
_ttlButton->moveToRight( _ttlButton->moveToRight(
-_ttlButton->width(), -_ttlButton->width(),
@ -1408,48 +1403,7 @@ void VoiceRecordBar::init() {
_showLockAnimation.start(std::move(callback), from, to, duration); _showLockAnimation.start(std::move(callback), from, to, duration);
}, lifetime()); }, lifetime());
_lock->setClickedCallback([=] { const auto setLevelAsSend = [=] {
if (!_lock->isStopState()) {
return;
}
::Media::Capture::instance()->startedChanges(
) | rpl::filter([=](bool capturing) {
return !capturing && _listen;
}) | rpl::take(1) | rpl::start_with_next([=] {
_lockShowing = false;
const auto to = 1.;
const auto &duration = st::historyRecordVoiceShowDuration;
auto callback = [=](float64 value) {
_listen->requestPaintProgress(value);
const auto reverseValue = to - value;
_level->requestPaintProgress(reverseValue);
update();
if (to == value) {
_recordingLifetime.destroy();
}
updateTTLGeometry(TTLAnimationType::TopBottom, 1. - value);
};
_showListenAnimation.stop();
_showListenAnimation.start(std::move(callback), 0., to, duration);
}, lifetime());
stopRecording(StopType::Listen);
});
_lock->locks(
) | rpl::start_with_next([=] {
if (_hasTTLFilter && _hasTTLFilter()) {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
}
_ttlButton->show();
}
updateTTLGeometry(TTLAnimationType::RightTopStatic, 0);
_level->setType(VoiceRecordButton::Type::Send); _level->setType(VoiceRecordButton::Type::Send);
_level->clicks( _level->clicks(
@ -1464,6 +1418,58 @@ void VoiceRecordBar::init() {
) | rpl::start_with_next([=](bool enter) { ) | rpl::start_with_next([=](bool enter) {
_inField = enter; _inField = enter;
}, _recordingLifetime); }, _recordingLifetime);
};
_lock->setClickedCallback([=] {
if (isListenState()) {
startRecording();
_listen = nullptr;
setLevelAsSend();
return;
}
if (!_lock->isStopState()) {
return;
}
stopRecording(StopType::Listen);
});
_paused.value() | rpl::distinct_until_changed(
) | rpl::start_with_next([=](bool paused) {
if (!paused) {
return;
}
// _lockShowing = false;
const auto to = 1.;
const auto &duration = st::historyRecordVoiceShowDuration;
auto callback = [=](float64 value) {
_listen->requestPaintProgress(value);
const auto reverseValue = to - value;
_level->requestPaintProgress(reverseValue);
update();
if (to == value) {
_recordingLifetime.destroy();
}
};
_showListenAnimation.stop();
_showListenAnimation.start(std::move(callback), 0., to, duration);
}, lifetime());
_lock->locks(
) | rpl::start_with_next([=] {
if (_hasTTLFilter && _hasTTLFilter()) {
if (!_ttlButton) {
_ttlButton = std::make_unique<TTLButton>(
_outerContainer,
_st);
}
_ttlButton->show();
}
updateTTLGeometry(TTLAnimationType::RightTopStatic, 0);
setLevelAsSend();
const auto &duration = st::historyRecordVoiceShowDuration; const auto &duration = st::historyRecordVoiceShowDuration;
const auto from = 0.; const auto from = 0.;
@ -1616,7 +1622,12 @@ void VoiceRecordBar::startRecording() {
startRedCircleAnimation(); startRedCircleAnimation();
_recording = true; _recording = true;
if (_paused.current()) {
_paused = false;
instance()->pause(false, nullptr);
} else {
instance()->start(); instance()->start();
}
instance()->updated( instance()->updated(
) | rpl::start_with_next_error([=](const Update &update) { ) | rpl::start_with_next_error([=](const Update &update) {
_recordingTipRequired = (update.samples < kMinSamples); _recordingTipRequired = (update.samples < kMinSamples);
@ -1685,7 +1696,7 @@ void VoiceRecordBar::stop(bool send) {
const auto type = send ? StopType::Send : StopType::Cancel; const auto type = send ? StopType::Send : StopType::Cancel;
stopRecording(type, ttlBeforeHide); stopRecording(type, ttlBeforeHide);
}; };
_lockShowing = false; // _lockShowing = false;
visibilityAnimate(false, std::move(disappearanceCallback)); visibilityAnimate(false, std::move(disappearanceCallback));
} }
@ -1695,6 +1706,7 @@ void VoiceRecordBar::finish() {
_inField = false; _inField = false;
_redCircleProgress = 0.; _redCircleProgress = 0.;
_recordingSamples = 0; _recordingSamples = 0;
_paused = false;
_showAnimation.stop(); _showAnimation.stop();
_lockToStopAnimation.stop(); _lockToStopAnimation.stop();
@ -1704,6 +1716,8 @@ void VoiceRecordBar::finish() {
[[maybe_unused]] const auto s = takeTTLState(); [[maybe_unused]] const auto s = takeTTLState();
_sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 }); _sendActionUpdates.fire({ Api::SendProgressType::RecordVoice, -1 });
_data = {};
} }
void VoiceRecordBar::hideFast() { void VoiceRecordBar::hideFast() {
@ -1719,43 +1733,53 @@ void VoiceRecordBar::stopRecording(StopType type, bool ttlBeforeHide) {
instance()->stop(crl::guard(this, [=](Result &&data) { instance()->stop(crl::guard(this, [=](Result &&data) {
_cancelRequests.fire({}); _cancelRequests.fire({});
})); }));
} else if (type == StopType::Listen) {
instance()->pause(true, crl::guard(this, [=](Result &&data) {
if (data.bytes.isEmpty()) {
// Close everything.
stop(false);
return; return;
} }
_paused = true;
_data = std::move(data);
window()->raise();
window()->activateWindow();
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
&_data,
_cancelFont);
_listenChanges.fire({});
// _lockShowing = false;
}));
} else if (type == StopType::Send) {
instance()->stop(crl::guard(this, [=](Result &&data) { instance()->stop(crl::guard(this, [=](Result &&data) {
if (data.bytes.isEmpty()) { if (data.bytes.isEmpty()) {
// Close everything. // Close everything.
stop(false); stop(false);
return; return;
} }
_data = std::move(data);
window()->raise(); window()->raise();
window()->activateWindow(); window()->activateWindow();
const auto duration = Duration(data.samples);
if (type == StopType::Send) {
const auto options = Api::SendOptions{ const auto options = Api::SendOptions{
.ttlSeconds = (ttlBeforeHide .ttlSeconds = (ttlBeforeHide
? std::numeric_limits<int>::max() ? std::numeric_limits<int>::max()
: 0), : 0),
}; };
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
data.bytes, _data.bytes,
data.waveform, _data.waveform,
duration, Duration(_data.samples),
options, options,
}); });
} else if (type == StopType::Listen) {
_listen = std::make_unique<ListenWrap>(
this,
_st,
&_show->session(),
std::move(data),
_cancelFont);
_listenChanges.fire({});
_lockShowing = false;
}
})); }));
} }
}
void VoiceRecordBar::drawDuration(QPainter &p) { void VoiceRecordBar::drawDuration(QPainter &p) {
const auto duration = FormatVoiceDuration(_recordingSamples); const auto duration = FormatVoiceDuration(_recordingSamples);
@ -1811,14 +1835,13 @@ void VoiceRecordBar::drawMessage(QPainter &p, float64 recordActive) {
void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) { void VoiceRecordBar::requestToSendWithOptions(Api::SendOptions options) {
if (isListenState()) { if (isListenState()) {
const auto data = _listen->data();
if (takeTTLState()) { if (takeTTLState()) {
options.ttlSeconds = std::numeric_limits<int>::max(); options.ttlSeconds = std::numeric_limits<int>::max();
} }
_sendVoiceRequests.fire({ _sendVoiceRequests.fire({
data->bytes, _data.bytes,
data->waveform, _data.waveform,
Duration(data->samples), Duration(_data.samples),
options, options,
}); });
} }
@ -1837,7 +1860,7 @@ rpl::producer<> VoiceRecordBar::cancelRequests() const {
} }
bool VoiceRecordBar::isRecording() const { bool VoiceRecordBar::isRecording() const {
return _recording.current(); return _recording.current() && !_paused.current();
} }
bool VoiceRecordBar::isRecordingLocked() const { bool VoiceRecordBar::isRecordingLocked() const {

View file

@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "api/api_common.h" #include "api/api_common.h"
#include "base/timer.h" #include "base/timer.h"
#include "history/view/controls/compose_controls_common.h" #include "history/view/controls/compose_controls_common.h"
#include "media/audio/media_audio_capture_common.h"
#include "ui/effects/animations.h" #include "ui/effects/animations.h"
#include "ui/round_rect.h" #include "ui/round_rect.h"
#include "ui/rp_widget.h" #include "ui/rp_widget.h"
@ -162,6 +163,9 @@ private:
std::unique_ptr<Ui::AbstractButton> _ttlButton; std::unique_ptr<Ui::AbstractButton> _ttlButton;
std::unique_ptr<ListenWrap> _listen; std::unique_ptr<ListenWrap> _listen;
::Media::Capture::Result _data;
rpl::variable<bool> _paused;
base::Timer _startTimer; base::Timer _startTimer;
rpl::event_stream<SendActionUpdate> _sendActionUpdates; rpl::event_stream<SendActionUpdate> _sendActionUpdates;

View file

@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/ */
#include "media/audio/media_audio_capture.h" #include "media/audio/media_audio_capture.h"
#include "media/audio/media_audio_capture_common.h"
#include "media/audio/media_audio_ffmpeg_loader.h" #include "media/audio/media_audio_ffmpeg_loader.h"
#include "ffmpeg/ffmpeg_utility.h" #include "ffmpeg/ffmpeg_utility.h"
#include "base/timer.h" #include "base/timer.h"
@ -37,6 +38,45 @@ bool ErrorHappened(ALCdevice *device) {
return false; return false;
} }
[[nodiscard]] VoiceWaveform CollectWaveform(
const QVector<uchar> &waveformVector) {
if (waveformVector.isEmpty()) {
return {};
}
auto waveform = VoiceWaveform();
auto count = int64(waveformVector.size());
auto sum = int64(0);
if (count >= Player::kWaveformSamplesCount) {
auto peaks = QVector<uint16>();
peaks.reserve(Player::kWaveformSamplesCount);
auto peak = uint16(0);
for (auto i = int32(0); i < count; ++i) {
auto sample = uint16(waveformVector.at(i)) * 256;
if (peak < sample) {
peak = sample;
}
sum += Player::kWaveformSamplesCount;
if (sum >= count) {
sum -= count;
peaks.push_back(peak);
peak = 0;
}
}
auto sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0LL);
peak = qMax(int32(sum * 1.8 / peaks.size()), 2500);
waveform.resize(peaks.size());
for (int32 i = 0, l = peaks.size(); i != l; ++i) {
waveform[i] = char(qMin(
31U,
uint32(qMin(peaks.at(i), peak)) * 31 / peak));
}
}
return waveform;
}
} // namespace } // namespace
class Instance::Inner final : public QObject { class Instance::Inner final : public QObject {
@ -46,6 +86,7 @@ public:
void start(Fn<void(Update)> updated, Fn<void()> error); void start(Fn<void(Update)> updated, Fn<void()> error);
void stop(Fn<void(Result&&)> callback = nullptr); void stop(Fn<void(Result&&)> callback = nullptr);
void pause(bool value, Fn<void(Result&&)> callback);
private: private:
void process(); void process();
@ -67,6 +108,8 @@ private:
base::Timer _timer; base::Timer _timer;
QByteArray _captured; QByteArray _captured;
bool _paused = false;
}; };
void Start() { void Start() {
@ -118,6 +161,17 @@ void Instance::stop(Fn<void(Result&&)> callback) {
}); });
} }
void Instance::pause(bool value, Fn<void(Result&&)> callback) {
Expects(callback != nullptr || !value);
InvokeQueued(_inner.get(), [=] {
_inner->pause(value, [=](Result &&result) {
crl::on_main([=, result = std::move(result)]() mutable {
callback(std::move(result));
});
});
});
}
void Instance::check() { void Instance::check() {
_available = false; _available = false;
if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) { if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {
@ -241,6 +295,9 @@ void Instance::Inner::fail() {
void Instance::Inner::start(Fn<void(Update)> updated, Fn<void()> error) { void Instance::Inner::start(Fn<void(Update)> updated, Fn<void()> error) {
_updated = std::move(updated); _updated = std::move(updated);
_error = std::move(error); _error = std::move(error);
if (_paused) {
_paused = false;
}
// Start OpenAL Capture // Start OpenAL Capture
d->device = alcCaptureOpenDevice(nullptr, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5); d->device = alcCaptureOpenDevice(nullptr, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5);
@ -404,10 +461,23 @@ void Instance::Inner::start(Fn<void(Update)> updated, Fn<void()> error) {
DEBUG_LOG(("Audio Capture: started!")); DEBUG_LOG(("Audio Capture: started!"));
} }
void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {
_paused = value;
if (!_paused) {
return;
}
callback({
d->fullSamples ? d->data : QByteArray(),
d->fullSamples ? CollectWaveform(d->waveform) : VoiceWaveform(),
qint32(d->fullSamples),
});
}
void Instance::Inner::stop(Fn<void(Result&&)> callback) { void Instance::Inner::stop(Fn<void(Result&&)> callback) {
if (!_timer.isActive()) { if (!_timer.isActive()) {
return; // in stop() already return; // in stop() already
} }
_paused = false;
_timer.cancel(); _timer.cancel();
const auto needResult = (callback != nullptr); const auto needResult = (callback != nullptr);
@ -480,33 +550,7 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) {
VoiceWaveform waveform; VoiceWaveform waveform;
qint32 samples = d->fullSamples; qint32 samples = d->fullSamples;
if (needResult && samples && !d->waveform.isEmpty()) { if (needResult && samples && !d->waveform.isEmpty()) {
int64 count = d->waveform.size(), sum = 0; waveform = CollectWaveform(d->waveform);
if (count >= Player::kWaveformSamplesCount) {
QVector<uint16> peaks;
peaks.reserve(Player::kWaveformSamplesCount);
uint16 peak = 0;
for (int32 i = 0; i < count; ++i) {
uint16 sample = uint16(d->waveform.at(i)) * 256;
if (peak < sample) {
peak = sample;
}
sum += Player::kWaveformSamplesCount;
if (sum >= count) {
sum -= count;
peaks.push_back(peak);
peak = 0;
}
}
auto sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0LL);
peak = qMax(int32(sum * 1.8 / peaks.size()), 2500);
waveform.resize(peaks.size());
for (int32 i = 0, l = peaks.size(); i != l; ++i) {
waveform[i] = char(qMin(31U, uint32(qMin(peaks.at(i), peak)) * 31 / peak));
}
}
} }
if (hadDevice) { if (hadDevice) {
if (d->codecContext) { if (d->codecContext) {
@ -568,6 +612,10 @@ void Instance::Inner::stop(Fn<void(Result&&)> callback) {
void Instance::Inner::process() { void Instance::Inner::process() {
Expects(!d->processing); Expects(!d->processing);
if (_paused) {
return;
}
d->processing = true; d->processing = true;
const auto guard = gsl::finally([&] { d->processing = false; }); const auto guard = gsl::finally([&] { d->processing = false; });

View file

@ -19,11 +19,7 @@ struct Update {
ushort level = 0; ushort level = 0;
}; };
struct Result { struct Result;
QByteArray bytes;
VoiceWaveform waveform;
int samples = 0;
};
void Start(); void Start();
void Finish(); void Finish();
@ -51,6 +47,7 @@ public:
void start(); void start();
void stop(Fn<void(Result&&)> callback = nullptr); void stop(Fn<void(Result&&)> callback = nullptr);
void pause(bool value, Fn<void(Result&&)> callback);
private: private:
class Inner; class Inner;

View file

@ -0,0 +1,18 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Media::Capture {
struct Result {
QByteArray bytes;
VoiceWaveform waveform;
int samples = 0;
};
} // namespace Media::Capture