Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
697fdd4294 | ||
|
|
4964b8b488 | ||
|
|
6ae68b337d | ||
|
|
28899a642b | ||
|
|
4518e94c8a | ||
|
|
59b0b0659a |
@@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,2,0
|
||||
PRODUCTVERSION 1,0,2,0
|
||||
FILEVERSION 1,0,3,0
|
||||
PRODUCTVERSION 1,0,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -51,10 +51,10 @@ BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileVersion", "1.0.2.0"
|
||||
VALUE "FileVersion", "1.0.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2017"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "1.0.2.0"
|
||||
VALUE "ProductVersion", "1.0.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,2,0
|
||||
PRODUCTVERSION 1,0,2,0
|
||||
FILEVERSION 1,0,3,0
|
||||
PRODUCTVERSION 1,0,3,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@@ -43,10 +43,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileDescription", "Telegram Updater"
|
||||
VALUE "FileVersion", "1.0.2.0"
|
||||
VALUE "FileVersion", "1.0.3.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2017"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "1.0.2.0"
|
||||
VALUE "ProductVersion", "1.0.3.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -195,9 +195,7 @@ namespace {
|
||||
Global::SetLocalPasscode(false);
|
||||
Global::RefLocalPasscodeChanged().notify();
|
||||
}
|
||||
if (audioPlayer()) {
|
||||
audioPlayer()->stopAndClear();
|
||||
}
|
||||
Media::Player::mixer()->stopAndClear();
|
||||
if (auto w = wnd()) {
|
||||
w->tempDirDelete(Local::ClearManagerAll);
|
||||
w->notifyClearFast();
|
||||
@@ -2436,7 +2434,7 @@ namespace {
|
||||
|
||||
void playSound() {
|
||||
if (Global::SoundNotify() && !Platform::Notifications::skipAudio()) {
|
||||
audioPlayNotify();
|
||||
Media::Player::PlayNotify();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1069,8 +1069,8 @@ void AppClass::checkMapVersion() {
|
||||
if (Local::oldMapVersion() < AppVersion) {
|
||||
if (Local::oldMapVersion()) {
|
||||
QString versionFeatures;
|
||||
if ((cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 1000001) {
|
||||
versionFeatures = QString::fromUtf8("\xe2\x80\x94 Resize chats list with mouse press-and-drag\n\xe2\x80\x94 Drag-n-drop images from Firefox fixed in Windows\n\xe2\x80\x94 Bug fixes and other minor improvements");
|
||||
if ((cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 1000003) {
|
||||
versionFeatures = QString::fromUtf8("\xe2\x80\x94 Audio device is opened only when some sound is played.\n\xe2\x80\x94 On Windows Vista and later audio device should switch after the system default changes.");
|
||||
} else if (!(cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 1000002) {
|
||||
versionFeatures = langNewVersionText();
|
||||
} else {
|
||||
|
||||
@@ -97,21 +97,11 @@ enum {
|
||||
MediaOverviewStartPerPage = 5,
|
||||
MediaOverviewPreloadCount = 4,
|
||||
|
||||
AudioSimultaneousLimit = 4,
|
||||
AudioCheckPositionTimeout = 100, // 100ms per check audio pos
|
||||
AudioCheckPositionDelta = 2400, // update position called each 2400 samples
|
||||
AudioFadeTimeout = 7, // 7ms
|
||||
AudioFadeDuration = 500,
|
||||
AudioVoiceMsgSkip = 400, // 200ms
|
||||
AudioVoiceMsgFade = 300, // 300ms
|
||||
AudioPreloadSamples = 2 * 48000, // preload next part if less than 5 seconds remains
|
||||
AudioVoiceMsgFrequency = 48000, // 48 kHz
|
||||
AudioVoiceMsgMaxLength = 100 * 60, // 100 minutes
|
||||
AudioVoiceMsgUpdateView = 100, // 100ms
|
||||
AudioVoiceMsgChannels = 2, // stereo
|
||||
AudioVoiceMsgBufferSize = 256 * 1024, // 256 Kb buffers (1.3 - 3.0 secs)
|
||||
AudioVoiceMsgInMemory = 2 * 1024 * 1024, // 2 Mb audio is hold in memory and auto loaded
|
||||
AudioPauseDeviceTimeout = 3000, // pause in 3 secs after playing is over
|
||||
|
||||
WaveformSamplesCount = 100,
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
|
||||
#define BETA_VERSION_MACRO (0ULL)
|
||||
|
||||
constexpr int AppVersion = 1000002;
|
||||
constexpr str_const AppVersionStr = "1.0.2";
|
||||
constexpr bool AppAlphaVersion = false;
|
||||
constexpr int AppVersion = 1000003;
|
||||
constexpr str_const AppVersionStr = "1.0.3";
|
||||
constexpr bool AppAlphaVersion = true;
|
||||
constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;
|
||||
|
||||
@@ -1436,48 +1436,45 @@ bool HistoryDocument::updateStatusText() const {
|
||||
} else if (_data->loading()) {
|
||||
statusSize = _data->loadOffset();
|
||||
} else if (_data->loaded()) {
|
||||
using State = Media::Player::State;
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
if (audioPlayer()) {
|
||||
if (_data->voice()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Voice);
|
||||
if (playing == AudioMsgId(_data, _parent->fullId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
if (auto voice = Get<HistoryDocumentVoice>()) {
|
||||
bool was = voice->_playback;
|
||||
voice->ensurePlayback(this);
|
||||
if (!was || playbackState.position != voice->_playback->_position) {
|
||||
float64 prg = playbackState.duration ? snap(float64(playbackState.position) / playbackState.duration, 0., 1.) : 0.;
|
||||
if (voice->_playback->_position < playbackState.position) {
|
||||
voice->_playback->a_progress.start(prg);
|
||||
} else {
|
||||
voice->_playback->a_progress = anim::value(0., prg);
|
||||
}
|
||||
voice->_playback->_position = playbackState.position;
|
||||
voice->_playback->_a_progress.start();
|
||||
if (_data->voice()) {
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
|
||||
if (state.id == AudioMsgId(_data, _parent->fullId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
if (auto voice = Get<HistoryDocumentVoice>()) {
|
||||
bool was = voice->_playback;
|
||||
voice->ensurePlayback(this);
|
||||
if (!was || state.position != voice->_playback->_position) {
|
||||
float64 prg = state.duration ? snap(float64(state.position) / state.duration, 0., 1.) : 0.;
|
||||
if (voice->_playback->_position < state.position) {
|
||||
voice->_playback->a_progress.start(prg);
|
||||
} else {
|
||||
voice->_playback->a_progress = anim::value(0., prg);
|
||||
}
|
||||
voice->_playback->_position = state.position;
|
||||
voice->_playback->_a_progress.start();
|
||||
}
|
||||
}
|
||||
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
} else {
|
||||
if (auto voice = Get<HistoryDocumentVoice>()) {
|
||||
voice->checkPlaybackFinished();
|
||||
}
|
||||
}
|
||||
} else if (_data->song()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing == AudioMsgId(_data, _parent->fullId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
} else {
|
||||
}
|
||||
if (!showPause && (playing == AudioMsgId(_data, _parent->fullId()))) {
|
||||
showPause = (Media::Player::exists() && Media::Player::instance()->isSeeking());
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
} else {
|
||||
if (auto voice = Get<HistoryDocumentVoice>()) {
|
||||
voice->checkPlaybackFinished();
|
||||
}
|
||||
}
|
||||
} else if (_data->song()) {
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id == AudioMsgId(_data, _parent->fullId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
} else {
|
||||
}
|
||||
if (!showPause && (state.id == AudioMsgId(_data, _parent->fullId()))) {
|
||||
showPause = Media::Player::instance()->isSeeking();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
statusSize = FileStatusSizeReady;
|
||||
|
||||
@@ -52,6 +52,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "mainwindow.h"
|
||||
#include "fileuploader.h"
|
||||
#include "media/media_audio.h"
|
||||
#include "media/media_audio_capture.h"
|
||||
#include "localstorage.h"
|
||||
#include "apiwrap.h"
|
||||
#include "window/top_bar_widget.h"
|
||||
@@ -3115,11 +3116,9 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent)
|
||||
connect(_emojiPan, SIGNAL(updateStickers()), this, SLOT(updateStickers()));
|
||||
connect(&_sendActionStopTimer, SIGNAL(timeout()), this, SLOT(onCancelSendAction()));
|
||||
connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreviewTimeout()));
|
||||
if (audioCapture()) {
|
||||
connect(audioCapture(), SIGNAL(error()), this, SLOT(onRecordError()));
|
||||
connect(audioCapture(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32)));
|
||||
connect(audioCapture(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32)));
|
||||
}
|
||||
connect(Media::Capture::instance(), SIGNAL(error()), this, SLOT(onRecordError()));
|
||||
connect(Media::Capture::instance(), SIGNAL(updated(quint16,qint32)), this, SLOT(onRecordUpdate(quint16,qint32)));
|
||||
connect(Media::Capture::instance(), SIGNAL(done(QByteArray,VoiceWaveform,qint32)), this, SLOT(onRecordDone(QByteArray,VoiceWaveform,qint32)));
|
||||
|
||||
_attachToggle->setClickedCallback(App::LambdaDelayed(st::historyAttach.ripple.hideDuration, this, [this] {
|
||||
chooseAttach();
|
||||
@@ -3533,7 +3532,7 @@ void HistoryWidget::onRecordDone(QByteArray result, VoiceWaveform waveform, qint
|
||||
if (!canWriteMessage() || result.isEmpty()) return;
|
||||
|
||||
App::wnd()->activateWindow();
|
||||
auto duration = samples / AudioVoiceMsgFrequency;
|
||||
auto duration = samples / Media::Player::kDefaultFrequency;
|
||||
auto to = FileLoadTo(_peer->id, _silent->checked(), replyToId());
|
||||
auto caption = QString();
|
||||
_fileLoader.addTask(MakeShared<FileLoadTask>(result, duration, waveform, to, caption));
|
||||
@@ -3548,7 +3547,7 @@ void HistoryWidget::onRecordUpdate(quint16 level, qint32 samples) {
|
||||
a_recordingLevel.start(level);
|
||||
_a_recording.start();
|
||||
_recordingSamples = samples;
|
||||
if (samples < 0 || samples >= AudioVoiceMsgFrequency * AudioVoiceMsgMaxLength) {
|
||||
if (samples < 0 || samples >= Media::Player::kDefaultFrequency * AudioVoiceMsgMaxLength) {
|
||||
stopRecording(_peer && samples > 0 && _inField);
|
||||
}
|
||||
updateField();
|
||||
@@ -5649,10 +5648,10 @@ void HistoryWidget::leaveToChildEvent(QEvent *e, QWidget *child) { // e -- from
|
||||
}
|
||||
|
||||
void HistoryWidget::recordStartCallback() {
|
||||
if (!cHasAudioCapture()) {
|
||||
if (!Media::Capture::instance()->available()) {
|
||||
return;
|
||||
}
|
||||
emit audioCapture()->start();
|
||||
emit Media::Capture::instance()->start();
|
||||
|
||||
_recording = _inField = true;
|
||||
updateControlsVisibility();
|
||||
@@ -5680,13 +5679,13 @@ void HistoryWidget::mouseReleaseEvent(QMouseEvent *e) {
|
||||
_attachDrag = DragStateNone;
|
||||
updateDragAreas();
|
||||
}
|
||||
if (_recording && cHasAudioCapture()) {
|
||||
if (_recording) {
|
||||
stopRecording(_peer && _inField);
|
||||
}
|
||||
}
|
||||
|
||||
void HistoryWidget::stopRecording(bool send) {
|
||||
emit audioCapture()->stop(send);
|
||||
emit Media::Capture::instance()->stop(send);
|
||||
|
||||
a_recordingLevel = anim::value();
|
||||
_a_recording.stop();
|
||||
@@ -6045,7 +6044,7 @@ bool HistoryWidget::isMuteUnmute() const {
|
||||
}
|
||||
|
||||
bool HistoryWidget::showRecordButton() const {
|
||||
return cHasAudioCapture() && !_field->hasSendText() && !readyToForward() && !_editMsgId;
|
||||
return Media::Capture::instance()->available() && !_field->hasSendText() && !readyToForward() && !_editMsgId;
|
||||
}
|
||||
|
||||
bool HistoryWidget::showInlineBotCancel() const {
|
||||
@@ -8737,7 +8736,7 @@ void HistoryWidget::drawRecording(Painter &p, float64 recordActive) {
|
||||
p.drawEllipse(_attachToggle->x() + (_attachEmoji->width() - d) / 2, _attachToggle->y() + (_attachToggle->height() - d) / 2, d, d);
|
||||
}
|
||||
|
||||
QString duration = formatDurationText(_recordingSamples / AudioVoiceMsgFrequency);
|
||||
auto duration = formatDurationText(_recordingSamples / Media::Player::kDefaultFrequency);
|
||||
p.setFont(st::historyRecordFont);
|
||||
|
||||
p.setPen(st::historyRecordDurationFg);
|
||||
|
||||
@@ -830,30 +830,25 @@ bool File::updateStatusText() const {
|
||||
} else if (document->loading()) {
|
||||
statusSize = document->loadOffset();
|
||||
} else if (document->loaded()) {
|
||||
using State = Media::Player::State;
|
||||
if (document->voice()) {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
if (audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Voice);
|
||||
if (playing == AudioMsgId(document, FullMsgId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
}
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
|
||||
if (state.id == AudioMsgId(document, FullMsgId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
}
|
||||
} else if (document->song()) {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
if (audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing == AudioMsgId(document, FullMsgId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
}
|
||||
if (!showPause && (playing == AudioMsgId(document, FullMsgId())) && Media::Player::exists() && Media::Player::instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id == AudioMsgId(document, FullMsgId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
}
|
||||
if (!showPause && (state.id == AudioMsgId(document, FullMsgId())) && Media::Player::instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
} else {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
|
||||
@@ -100,13 +100,11 @@ MainWidget::MainWidget(QWidget *parent) : TWidget(parent)
|
||||
connect(_topBar, SIGNAL(clicked()), this, SLOT(onTopBarClick()));
|
||||
connect(_history, SIGNAL(historyShown(History*,MsgId)), this, SLOT(onHistoryShown(History*,MsgId)));
|
||||
connect(&updateNotifySettingTimer, SIGNAL(timeout()), this, SLOT(onUpdateNotifySettings()));
|
||||
if (auto player = audioPlayer()) {
|
||||
subscribe(player, [this](const AudioMsgId &audioId) {
|
||||
if (audioId.type() != AudioMsgId::Type::Video) {
|
||||
handleAudioUpdate(audioId);
|
||||
}
|
||||
});
|
||||
}
|
||||
subscribe(Media::Player::Updated(), [this](const AudioMsgId &audioId) {
|
||||
if (audioId.type() != AudioMsgId::Type::Video) {
|
||||
handleAudioUpdate(audioId);
|
||||
}
|
||||
});
|
||||
|
||||
subscribe(Global::RefDialogsListFocused(), [this](bool) {
|
||||
updateDialogsWidthAnimated();
|
||||
@@ -133,30 +131,28 @@ MainWidget::MainWidget(QWidget *parent) : TWidget(parent)
|
||||
});
|
||||
connect(&_cacheBackgroundTimer, SIGNAL(timeout()), this, SLOT(onCacheBackground()));
|
||||
|
||||
if (Media::Player::exists()) {
|
||||
_playerPanel->setPinCallback([this] { switchToFixedPlayer(); });
|
||||
_playerPanel->setCloseCallback([this] { closeBothPlayers(); });
|
||||
subscribe(Media::Player::instance()->titleButtonOver(), [this](bool over) {
|
||||
if (over) {
|
||||
_playerPanel->showFromOther();
|
||||
} else {
|
||||
_playerPanel->hideFromOther();
|
||||
_playerPanel->setPinCallback([this] { switchToFixedPlayer(); });
|
||||
_playerPanel->setCloseCallback([this] { closeBothPlayers(); });
|
||||
subscribe(Media::Player::instance()->titleButtonOver(), [this](bool over) {
|
||||
if (over) {
|
||||
_playerPanel->showFromOther();
|
||||
} else {
|
||||
_playerPanel->hideFromOther();
|
||||
}
|
||||
});
|
||||
subscribe(Media::Player::instance()->playerWidgetOver(), [this](bool over) {
|
||||
if (over) {
|
||||
if (_playerPlaylist->isHidden()) {
|
||||
auto position = mapFromGlobal(QCursor::pos()).x();
|
||||
auto bestPosition = _playerPlaylist->bestPositionFor(position);
|
||||
if (rtl()) bestPosition = position + 2 * (position - bestPosition) - _playerPlaylist->width();
|
||||
updateMediaPlaylistPosition(bestPosition);
|
||||
}
|
||||
});
|
||||
subscribe(Media::Player::instance()->playerWidgetOver(), [this](bool over) {
|
||||
if (over) {
|
||||
if (_playerPlaylist->isHidden()) {
|
||||
auto position = mapFromGlobal(QCursor::pos()).x();
|
||||
auto bestPosition = _playerPlaylist->bestPositionFor(position);
|
||||
if (rtl()) bestPosition = position + 2 * (position - bestPosition) - _playerPlaylist->width();
|
||||
updateMediaPlaylistPosition(bestPosition);
|
||||
}
|
||||
_playerPlaylist->showFromOther();
|
||||
} else {
|
||||
_playerPlaylist->hideFromOther();
|
||||
}
|
||||
});
|
||||
}
|
||||
_playerPlaylist->showFromOther();
|
||||
} else {
|
||||
_playerPlaylist->hideFromOther();
|
||||
}
|
||||
});
|
||||
|
||||
subscribe(Adaptive::Changed(), [this]() { handleAdaptiveLayoutUpdate(); });
|
||||
|
||||
@@ -1567,11 +1563,11 @@ void MainWidget::ui_autoplayMediaInlineAsync(qint32 channelId, qint32 msgId) {
|
||||
}
|
||||
|
||||
void MainWidget::handleAudioUpdate(const AudioMsgId &audioId) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, audioId.type());
|
||||
if (playing == audioId && playbackState.state == AudioPlayerStoppedAtStart) {
|
||||
playbackState.state = AudioPlayerStopped;
|
||||
audioPlayer()->clearStoppedAtStart(audioId);
|
||||
using State = Media::Player::State;
|
||||
auto state = Media::Player::mixer()->currentState(audioId.type());
|
||||
if (state.id == audioId && state.state == State::StoppedAtStart) {
|
||||
state.state = State::Stopped;
|
||||
Media::Player::mixer()->clearStoppedAtStart(audioId);
|
||||
|
||||
auto document = audioId.audio();
|
||||
auto filepath = document->filepath(DocumentData::FilePathResolveSaveFromData);
|
||||
@@ -1582,9 +1578,9 @@ void MainWidget::handleAudioUpdate(const AudioMsgId &audioId) {
|
||||
}
|
||||
}
|
||||
|
||||
if (playing == audioId && audioId.type() == AudioMsgId::Type::Song) {
|
||||
if (!(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
if (!_playerUsingPanel && !_player && Media::Player::exists()) {
|
||||
if (state.id == audioId && audioId.type() == AudioMsgId::Type::Song) {
|
||||
if (!Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
if (!_playerUsingPanel && !_player) {
|
||||
createPlayer();
|
||||
}
|
||||
} else if (_player && _player->isHidden() && !_playerUsingPanel) {
|
||||
@@ -1642,15 +1638,10 @@ void MainWidget::closeBothPlayers() {
|
||||
}
|
||||
_playerVolume.destroyDelayed();
|
||||
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->usePanelPlayer().notify(false, true);
|
||||
}
|
||||
Media::Player::instance()->usePanelPlayer().notify(false, true);
|
||||
_playerPanel->hideIgnoringEnterEvents();
|
||||
_playerPlaylist->hideIgnoringEnterEvents();
|
||||
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->stop();
|
||||
}
|
||||
Media::Player::instance()->stop();
|
||||
|
||||
Shortcuts::disableMediaShortcuts();
|
||||
}
|
||||
@@ -1684,9 +1675,8 @@ void MainWidget::playerHeightUpdated() {
|
||||
updateControlsGeometry();
|
||||
}
|
||||
if (!_playerHeight && _player->isHidden()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing && (playbackState.state & AudioPlayerStoppedMask)) {
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id && Media::Player::IsStopped(state.state)) {
|
||||
_playerVolume.destroyDelayed();
|
||||
_player.destroyDelayed();
|
||||
}
|
||||
@@ -1714,9 +1704,7 @@ void MainWidget::documentLoadProgress(DocumentData *document) {
|
||||
App::wnd()->documentUpdated(document);
|
||||
|
||||
if (!document->loaded() && document->song()) {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->documentLoadProgress(document);
|
||||
}
|
||||
Media::Player::instance()->documentLoadProgress(document);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,70 +20,106 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "core/basic_types.h"
|
||||
|
||||
void audioInit();
|
||||
bool audioWorks();
|
||||
void audioPlayNotify();
|
||||
void audioFinish();
|
||||
|
||||
enum AudioPlayerState {
|
||||
AudioPlayerStopped = 0x01,
|
||||
AudioPlayerStoppedAtEnd = 0x02,
|
||||
AudioPlayerStoppedAtError = 0x03,
|
||||
AudioPlayerStoppedAtStart = 0x04,
|
||||
AudioPlayerStoppedMask = 0x07,
|
||||
|
||||
AudioPlayerStarting = 0x08,
|
||||
AudioPlayerPlaying = 0x10,
|
||||
AudioPlayerFinishing = 0x18,
|
||||
AudioPlayerPausing = 0x20,
|
||||
AudioPlayerPaused = 0x28,
|
||||
AudioPlayerPausedAtEnd = 0x30,
|
||||
AudioPlayerResuming = 0x38,
|
||||
};
|
||||
|
||||
class AudioPlayerFader;
|
||||
class AudioPlayerLoaders;
|
||||
|
||||
struct VideoSoundData;
|
||||
struct VideoSoundPart;
|
||||
struct AudioPlaybackState {
|
||||
AudioPlayerState state = AudioPlayerStopped;
|
||||
int64 position = 0;
|
||||
TimeMs duration = 0;
|
||||
int32 frequency = 0;
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
constexpr auto kDefaultFrequency = 48000; // 48 kHz
|
||||
constexpr auto kTogetherLimit = 4;
|
||||
|
||||
class Fader;
|
||||
class Loaders;
|
||||
|
||||
void InitAudio();
|
||||
void DeInitAudio();
|
||||
|
||||
base::Observable<AudioMsgId> &Updated();
|
||||
void DetachFromDeviceByTimer();
|
||||
|
||||
void PlayNotify();
|
||||
|
||||
float64 ComputeVolume(AudioMsgId::Type type);
|
||||
|
||||
enum class State {
|
||||
Stopped = 0x01,
|
||||
StoppedAtEnd = 0x02,
|
||||
StoppedAtError = 0x03,
|
||||
StoppedAtStart = 0x04,
|
||||
|
||||
Starting = 0x08,
|
||||
Playing = 0x10,
|
||||
Finishing = 0x18,
|
||||
Pausing = 0x20,
|
||||
Paused = 0x28,
|
||||
PausedAtEnd = 0x30,
|
||||
Resuming = 0x38,
|
||||
};
|
||||
|
||||
class AudioPlayer : public QObject, public base::Observable<AudioMsgId>, private base::Subscriber {
|
||||
inline bool IsStopped(State state) {
|
||||
return (state == State::Stopped)
|
||||
|| (state == State::StoppedAtEnd)
|
||||
|| (state == State::StoppedAtError)
|
||||
|| (state == State::StoppedAtStart);
|
||||
}
|
||||
|
||||
inline bool IsPaused(State state) {
|
||||
return (state == State::Paused)
|
||||
|| (state == State::PausedAtEnd);
|
||||
}
|
||||
|
||||
inline bool IsFading(State state) {
|
||||
return (state == State::Starting)
|
||||
|| (state == State::Finishing)
|
||||
|| (state == State::Pausing)
|
||||
|| (state == State::Resuming);
|
||||
}
|
||||
|
||||
inline bool IsActive(State state) {
|
||||
return !IsStopped(state) && !IsPaused(state);
|
||||
}
|
||||
|
||||
struct TrackState {
|
||||
AudioMsgId id;
|
||||
State state = State::Stopped;
|
||||
int64 position = 0;
|
||||
TimeMs duration = 0;
|
||||
int frequency = kDefaultFrequency;
|
||||
};
|
||||
|
||||
class Mixer : public QObject, private base::Subscriber {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioPlayer();
|
||||
Mixer();
|
||||
|
||||
void play(const AudioMsgId &audio, int64 position = 0);
|
||||
void pauseresume(AudioMsgId::Type type, bool fast = false);
|
||||
void seek(int64 position); // type == AudioMsgId::Type::Song
|
||||
void seek(AudioMsgId::Type type, int64 position); // type == AudioMsgId::Type::Song
|
||||
void stop(AudioMsgId::Type type);
|
||||
|
||||
// Video player audio stream interface.
|
||||
void initFromVideo(uint64 videoPlayId, std_::unique_ptr<VideoSoundData> &&data, int64 position);
|
||||
void feedFromVideo(VideoSoundPart &&part);
|
||||
int64 getVideoCorrectedTime(uint64 playId, TimeMs frameMs, TimeMs systemMs);
|
||||
AudioPlaybackState currentVideoState(uint64 videoPlayId);
|
||||
TrackState currentVideoState(uint64 videoPlayId);
|
||||
void stopFromVideo(uint64 videoPlayId);
|
||||
void pauseFromVideo(uint64 videoPlayId);
|
||||
void resumeFromVideo(uint64 videoPlayId);
|
||||
|
||||
void stopAndClear();
|
||||
|
||||
AudioPlaybackState currentState(AudioMsgId *audio, AudioMsgId::Type type);
|
||||
TrackState currentState(AudioMsgId::Type type);
|
||||
|
||||
void clearStoppedAtStart(const AudioMsgId &audio);
|
||||
|
||||
void resumeDevice();
|
||||
void detachFromDeviceByTimer();
|
||||
void detachTracks();
|
||||
void reattachIfNeeded();
|
||||
void reattachTracks();
|
||||
|
||||
~AudioPlayer();
|
||||
~Mixer();
|
||||
|
||||
private slots:
|
||||
void onError(const AudioMsgId &audio);
|
||||
@@ -105,55 +141,70 @@ signals:
|
||||
|
||||
private:
|
||||
bool fadedStop(AudioMsgId::Type type, bool *fadedStart = 0);
|
||||
bool updateCurrentStarted(AudioMsgId::Type type, int32 pos = -1);
|
||||
void resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered = -1);
|
||||
bool checkCurrentALError(AudioMsgId::Type type);
|
||||
|
||||
void videoSoundProgress(const AudioMsgId &audio);
|
||||
|
||||
struct AudioMsg {
|
||||
void clear();
|
||||
class Track {
|
||||
public:
|
||||
static constexpr int kBuffersCount = 3;
|
||||
|
||||
AudioMsgId audio;
|
||||
void reattach(AudioMsgId::Type type);
|
||||
void detach();
|
||||
void clear();
|
||||
void started();
|
||||
|
||||
bool isStreamCreated() const;
|
||||
void ensureStreamCreated();
|
||||
|
||||
int getNotQueuedBufferIndex();
|
||||
|
||||
TrackState state;
|
||||
|
||||
FileLocation file;
|
||||
QByteArray data;
|
||||
AudioPlaybackState playbackState = defaultState();
|
||||
int64 skipStart = 0;
|
||||
int64 skipEnd = 0;
|
||||
int64 bufferedPosition = 0;
|
||||
int64 bufferedLength = 0;
|
||||
bool loading = false;
|
||||
int64 started = 0;
|
||||
bool loaded = false;
|
||||
int64 fadeStartPosition = 0;
|
||||
|
||||
uint32 source = 0;
|
||||
int32 nextBuffer = 0;
|
||||
uint32 buffers[3] = { 0 };
|
||||
int64 samplesCount[3] = { 0 };
|
||||
int32 format = 0;
|
||||
int32 frequency = kDefaultFrequency;
|
||||
int samplesCount[kBuffersCount] = { 0 };
|
||||
QByteArray bufferSamples[kBuffersCount];
|
||||
|
||||
struct Stream {
|
||||
uint32 source = 0;
|
||||
uint32 buffers[kBuffersCount] = { 0 };
|
||||
};
|
||||
Stream stream;
|
||||
|
||||
uint64 videoPlayId = 0;
|
||||
std_::unique_ptr<VideoSoundData> videoData;
|
||||
|
||||
private:
|
||||
static AudioPlaybackState defaultState() {
|
||||
AudioPlaybackState result;
|
||||
result.frequency = AudioVoiceMsgFrequency;
|
||||
return result;
|
||||
}
|
||||
void createStream();
|
||||
void destroyStream();
|
||||
void resetStream();
|
||||
|
||||
};
|
||||
|
||||
void setStoppedState(AudioMsg *current, AudioPlayerState state = AudioPlayerStopped);
|
||||
void setStoppedState(Track *current, State state = State::Stopped);
|
||||
|
||||
AudioMsg *dataForType(AudioMsgId::Type type, int index = -1); // -1 uses currentIndex(type)
|
||||
const AudioMsg *dataForType(AudioMsgId::Type type, int index = -1) const;
|
||||
Track *trackForType(AudioMsgId::Type type, int index = -1); // -1 uses currentIndex(type)
|
||||
const Track *trackForType(AudioMsgId::Type type, int index = -1) const;
|
||||
int *currentIndex(AudioMsgId::Type type);
|
||||
const int *currentIndex(AudioMsgId::Type type) const;
|
||||
|
||||
int _audioCurrent = 0;
|
||||
AudioMsg _audioData[AudioSimultaneousLimit];
|
||||
Track _audioTracks[kTogetherLimit];
|
||||
|
||||
int _songCurrent = 0;
|
||||
AudioMsg _songData[AudioSimultaneousLimit];
|
||||
Track _songTracks[kTogetherLimit];
|
||||
|
||||
AudioMsg _videoData;
|
||||
Track _videoTrack;
|
||||
uint64 _lastVideoPlayId = 0;
|
||||
TimeMs _lastVideoPlaybackWhen = 0;
|
||||
TimeMs _lastVideoPlaybackCorrectedMs = 0;
|
||||
@@ -161,61 +212,23 @@ private:
|
||||
|
||||
QMutex _mutex;
|
||||
|
||||
friend class AudioPlayerFader;
|
||||
friend class AudioPlayerLoaders;
|
||||
friend class Fader;
|
||||
friend class Loaders;
|
||||
|
||||
QThread _faderThread, _loaderThread;
|
||||
AudioPlayerFader *_fader;
|
||||
AudioPlayerLoaders *_loader;
|
||||
Fader *_fader;
|
||||
Loaders *_loader;
|
||||
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
Mixer *mixer();
|
||||
|
||||
QMutex *audioPlayerMutex();
|
||||
float64 audioSuppressGain();
|
||||
float64 audioSuppressSongGain();
|
||||
bool audioCheckError();
|
||||
|
||||
} // namespace internal
|
||||
|
||||
class AudioCaptureInner;
|
||||
|
||||
class AudioCapture : public QObject {
|
||||
class Fader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioCapture();
|
||||
|
||||
bool check();
|
||||
|
||||
~AudioCapture();
|
||||
|
||||
signals:
|
||||
void start();
|
||||
void stop(bool needResult);
|
||||
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void error();
|
||||
|
||||
private:
|
||||
friend class AudioCaptureInner;
|
||||
|
||||
QThread _captureThread;
|
||||
AudioCaptureInner *_capture;
|
||||
|
||||
};
|
||||
|
||||
AudioPlayer *audioPlayer();
|
||||
AudioCapture *audioCapture();
|
||||
|
||||
class AudioPlayerFader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioPlayerFader(QThread *thread);
|
||||
void resumeDevice();
|
||||
Fader(QThread *thread);
|
||||
void keepAttachedToDevice();
|
||||
|
||||
signals:
|
||||
void error(const AudioMsgId &audio);
|
||||
@@ -223,13 +236,12 @@ signals:
|
||||
void audioStopped(const AudioMsgId &audio);
|
||||
void needToPreload(const AudioMsgId &audio);
|
||||
|
||||
void stopPauseDevice();
|
||||
|
||||
public slots:
|
||||
void onDetachFromDeviceByTimer(bool force);
|
||||
|
||||
void onInit();
|
||||
void onTimer();
|
||||
void onPauseTimer();
|
||||
void onPauseTimerStop();
|
||||
void onDetachFromDeviceTimer();
|
||||
|
||||
void onSuppressSong();
|
||||
void onUnsuppressSong();
|
||||
@@ -244,61 +256,39 @@ private:
|
||||
EmitPositionUpdated = 0x04,
|
||||
EmitNeedToPreload = 0x08,
|
||||
};
|
||||
int32 updateOnePlayback(AudioPlayer::AudioMsg *m, bool &hasPlaying, bool &hasFading, float64 suppressGain, bool suppressGainChanged);
|
||||
void setStoppedState(AudioPlayer::AudioMsg *m, AudioPlayerState state = AudioPlayerStopped);
|
||||
int32 updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasFading, float64 suppressGain, bool suppressGainChanged);
|
||||
void setStoppedState(Mixer::Track *track, State state = State::Stopped);
|
||||
|
||||
QTimer _timer, _pauseTimer;
|
||||
QMutex _pauseMutex;
|
||||
bool _pauseFlag = false;
|
||||
bool _paused = true;
|
||||
QTimer _timer;
|
||||
|
||||
bool _suppressAll = false;
|
||||
bool _suppressAllAnim = false;
|
||||
bool _suppressSong = false;
|
||||
bool _suppressSongAnim = false;
|
||||
bool _songVolumeChanged, _videoVolumeChanged;
|
||||
bool _songVolumeChanged = false;
|
||||
bool _videoVolumeChanged = false;
|
||||
anim::value _suppressAllGain, _suppressSongGain;
|
||||
TimeMs _suppressAllStart = 0;
|
||||
TimeMs _suppressSongStart = 0;
|
||||
|
||||
};
|
||||
|
||||
struct AudioCapturePrivate;
|
||||
struct AVFrame;
|
||||
|
||||
class AudioCaptureInner : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioCaptureInner(QThread *thread);
|
||||
~AudioCaptureInner();
|
||||
|
||||
signals:
|
||||
void error();
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
|
||||
public slots:
|
||||
void onInit();
|
||||
void onStart();
|
||||
void onStop(bool needResult);
|
||||
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
void processFrame(int32 offset, int32 framesize);
|
||||
|
||||
void writeFrame(AVFrame *frame);
|
||||
|
||||
// Writes the packets till EAGAIN is got from av_receive_packet()
|
||||
// Returns number of packets written or -1 on error
|
||||
int writePackets();
|
||||
|
||||
AudioCapturePrivate *d;
|
||||
QTimer _timer;
|
||||
QByteArray _captured;
|
||||
QTimer _detachFromDeviceTimer;
|
||||
QMutex _detachFromDeviceMutex;
|
||||
bool _detachFromDeviceForce = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
namespace internal {
|
||||
|
||||
QMutex *audioPlayerMutex();
|
||||
bool audioCheckError();
|
||||
|
||||
// AudioMutex must be locked.
|
||||
bool CheckAudioDeviceConnected();
|
||||
|
||||
} // namespace internal
|
||||
|
||||
MTPDocumentAttribute audioReadSongAttributes(const QString &fname, const QByteArray &data, QImage &cover, QByteArray &coverBytes, QByteArray &coverFormat);
|
||||
VoiceWaveform audioCountWaveform(const FileLocation &file, const QByteArray &data);
|
||||
|
||||
707
Telegram/SourceFiles/media/media_audio_capture.cpp
Normal file
707
Telegram/SourceFiles/media/media_audio_capture.cpp
Normal file
@@ -0,0 +1,707 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#include "stdafx.h"
|
||||
#include "media/media_audio_capture.h"
|
||||
|
||||
#include "media/media_audio_ffmpeg_loader.h"
|
||||
|
||||
#include <AL/al.h>
|
||||
#include <AL/alc.h>
|
||||
|
||||
#define AL_ALEXT_PROTOTYPES
|
||||
#include <AL/alext.h>
|
||||
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
namespace {
|
||||
|
||||
constexpr auto kCaptureFrequency = Player::kDefaultFrequency;
|
||||
constexpr auto kCaptureSkipDuration = TimeMs(400);
|
||||
constexpr auto kCaptureFadeInDuration = TimeMs(300);
|
||||
|
||||
Instance *CaptureInstance = nullptr;
|
||||
|
||||
bool ErrorHappened(ALCdevice *device) {
|
||||
ALenum errCode;
|
||||
if ((errCode = alcGetError(device)) != ALC_NO_ERROR) {
|
||||
LOG(("Audio Capture Error: %1, %2").arg(errCode).arg((const char *)alcGetString(device, errCode)));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Init() {
|
||||
t_assert(CaptureInstance == nullptr);
|
||||
CaptureInstance = new Instance();
|
||||
instance()->check();
|
||||
}
|
||||
|
||||
void DeInit() {
|
||||
delete base::take(CaptureInstance);
|
||||
}
|
||||
|
||||
Instance::Instance() : _inner(new Inner(&_thread)) {
|
||||
CaptureInstance = this;
|
||||
connect(this, SIGNAL(start()), _inner, SLOT(onStart()));
|
||||
connect(this, SIGNAL(stop(bool)), _inner, SLOT(onStop(bool)));
|
||||
connect(_inner, SIGNAL(done(QByteArray, VoiceWaveform, qint32)), this, SIGNAL(done(QByteArray, VoiceWaveform, qint32)));
|
||||
connect(_inner, SIGNAL(updated(quint16, qint32)), this, SIGNAL(updated(quint16, qint32)));
|
||||
connect(_inner, SIGNAL(error()), this, SIGNAL(error()));
|
||||
connect(&_thread, SIGNAL(started()), _inner, SLOT(onInit()));
|
||||
connect(&_thread, SIGNAL(finished()), _inner, SLOT(deleteLater()));
|
||||
_thread.start();
|
||||
}
|
||||
|
||||
void Instance::check() {
|
||||
_available = false;
|
||||
if (auto defaultDevice = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {
|
||||
if (auto device = alcCaptureOpenDevice(defaultDevice, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5)) {
|
||||
auto error = ErrorHappened(device);
|
||||
alcCaptureCloseDevice(device);
|
||||
_available = !error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
_inner = nullptr;
|
||||
_thread.quit();
|
||||
_thread.wait();
|
||||
}
|
||||
|
||||
Instance *instance() {
|
||||
return CaptureInstance;
|
||||
}
|
||||
|
||||
struct Instance::Inner::Private {
|
||||
ALCdevice *device = nullptr;
|
||||
AVOutputFormat *fmt = nullptr;
|
||||
uchar *ioBuffer = nullptr;
|
||||
AVIOContext *ioContext = nullptr;
|
||||
AVFormatContext *fmtContext = nullptr;
|
||||
AVStream *stream = nullptr;
|
||||
AVCodec *codec = nullptr;
|
||||
AVCodecContext *codecContext = nullptr;
|
||||
bool opened = false;
|
||||
|
||||
int srcSamples = 0;
|
||||
int dstSamples = 0;
|
||||
int maxDstSamples = 0;
|
||||
int dstSamplesSize = 0;
|
||||
int fullSamples = 0;
|
||||
uint8_t **srcSamplesData = nullptr;
|
||||
uint8_t **dstSamplesData = nullptr;
|
||||
SwrContext *swrContext = nullptr;
|
||||
|
||||
int32 lastUpdate = 0;
|
||||
uint16 levelMax = 0;
|
||||
|
||||
QByteArray data;
|
||||
int32 dataPos = 0;
|
||||
|
||||
int64 waveformMod = 0;
|
||||
int64 waveformEach = (kCaptureFrequency / 100);
|
||||
uint16 waveformPeak = 0;
|
||||
QVector<uchar> waveform;
|
||||
|
||||
static int _read_data(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto l = reinterpret_cast<Private*>(opaque);
|
||||
|
||||
int32 nbytes = qMin(l->data.size() - l->dataPos, int32(buf_size));
|
||||
if (nbytes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
memcpy(buf, l->data.constData() + l->dataPos, nbytes);
|
||||
l->dataPos += nbytes;
|
||||
return nbytes;
|
||||
}
|
||||
|
||||
static int _write_data(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto l = reinterpret_cast<Private*>(opaque);
|
||||
|
||||
if (buf_size <= 0) return 0;
|
||||
if (l->dataPos + buf_size > l->data.size()) l->data.resize(l->dataPos + buf_size);
|
||||
memcpy(l->data.data() + l->dataPos, buf, buf_size);
|
||||
l->dataPos += buf_size;
|
||||
return buf_size;
|
||||
}
|
||||
|
||||
static int64_t _seek_data(void *opaque, int64_t offset, int whence) {
|
||||
auto l = reinterpret_cast<Private*>(opaque);
|
||||
|
||||
int32 newPos = -1;
|
||||
switch (whence) {
|
||||
case SEEK_SET: newPos = offset; break;
|
||||
case SEEK_CUR: newPos = l->dataPos + offset; break;
|
||||
case SEEK_END: newPos = l->data.size() + offset; break;
|
||||
}
|
||||
if (newPos < 0) {
|
||||
return -1;
|
||||
}
|
||||
l->dataPos = newPos;
|
||||
return l->dataPos;
|
||||
}
|
||||
};
|
||||
|
||||
Instance::Inner::Inner(QThread *thread) : d(new Private()) {
|
||||
moveToThread(thread);
|
||||
_timer.moveToThread(thread);
|
||||
connect(&_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
|
||||
}
|
||||
|
||||
Instance::Inner::~Inner() {
|
||||
onStop(false);
|
||||
delete d;
|
||||
}
|
||||
|
||||
void Instance::Inner::onInit() {
|
||||
}
|
||||
|
||||
void Instance::Inner::onStart() {
|
||||
|
||||
// Start OpenAL Capture
|
||||
const ALCchar *dName = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER);
|
||||
DEBUG_LOG(("Audio Info: Capture device name '%1'").arg(dName));
|
||||
d->device = alcCaptureOpenDevice(dName, kCaptureFrequency, AL_FORMAT_MONO16, kCaptureFrequency / 5);
|
||||
if (!d->device) {
|
||||
LOG(("Audio Error: capture device not present!"));
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
alcCaptureStart(d->device);
|
||||
if (ErrorHappened(d->device)) {
|
||||
alcCaptureCloseDevice(d->device);
|
||||
d->device = nullptr;
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create encoding context
|
||||
|
||||
d->ioBuffer = (uchar*)av_malloc(AVBlockSize);
|
||||
|
||||
d->ioContext = avio_alloc_context(d->ioBuffer, AVBlockSize, 1, static_cast<void*>(d), &Private::_read_data, &Private::_write_data, &Private::_seek_data);
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
AVOutputFormat *fmt = 0;
|
||||
while ((fmt = av_oformat_next(fmt))) {
|
||||
if (fmt->name == qstr("opus")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fmt) {
|
||||
LOG(("Audio Error: Unable to find opus AVOutputFormat for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((res = avformat_alloc_output_context2(&d->fmtContext, fmt, 0, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_alloc_output_context2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
d->fmtContext->pb = d->ioContext;
|
||||
d->fmtContext->flags |= AVFMT_FLAG_CUSTOM_IO;
|
||||
d->opened = true;
|
||||
|
||||
// Add audio stream
|
||||
d->codec = avcodec_find_encoder(fmt->audio_codec);
|
||||
if (!d->codec) {
|
||||
LOG(("Audio Error: Unable to avcodec_find_encoder for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
d->stream = avformat_new_stream(d->fmtContext, d->codec);
|
||||
if (!d->stream) {
|
||||
LOG(("Audio Error: Unable to avformat_new_stream for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
d->stream->id = d->fmtContext->nb_streams - 1;
|
||||
d->codecContext = avcodec_alloc_context3(d->codec);
|
||||
if (!d->codecContext) {
|
||||
LOG(("Audio Error: Unable to avcodec_alloc_context3 for capture"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0);
|
||||
|
||||
d->codecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
|
||||
d->codecContext->bit_rate = 64000;
|
||||
d->codecContext->channel_layout = AV_CH_LAYOUT_MONO;
|
||||
d->codecContext->sample_rate = kCaptureFrequency;
|
||||
d->codecContext->channels = 1;
|
||||
|
||||
if (d->fmtContext->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||
d->codecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
|
||||
}
|
||||
|
||||
// Open audio stream
|
||||
if ((res = avcodec_open2(d->codecContext, d->codec, nullptr)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_open2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Alloc source samples
|
||||
|
||||
d->srcSamples = (d->codecContext->codec->capabilities & CODEC_CAP_VARIABLE_FRAME_SIZE) ? 10000 : d->codecContext->frame_size;
|
||||
//if ((res = av_samples_alloc_array_and_samples(&d->srcSamplesData, 0, d->codecContext->channels, d->srcSamples, d->codecContext->sample_fmt, 0)) < 0) {
|
||||
// LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
// onStop(false);
|
||||
// emit error();
|
||||
// return;
|
||||
//}
|
||||
// Using _captured directly
|
||||
|
||||
// Prepare resampling
|
||||
d->swrContext = swr_alloc();
|
||||
if (!d->swrContext) {
|
||||
fprintf(stderr, "Could not allocate resampler context\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
av_opt_set_int(d->swrContext, "in_channel_count", d->codecContext->channels, 0);
|
||||
av_opt_set_int(d->swrContext, "in_sample_rate", d->codecContext->sample_rate, 0);
|
||||
av_opt_set_sample_fmt(d->swrContext, "in_sample_fmt", AV_SAMPLE_FMT_S16, 0);
|
||||
av_opt_set_int(d->swrContext, "out_channel_count", d->codecContext->channels, 0);
|
||||
av_opt_set_int(d->swrContext, "out_sample_rate", d->codecContext->sample_rate, 0);
|
||||
av_opt_set_sample_fmt(d->swrContext, "out_sample_fmt", d->codecContext->sample_fmt, 0);
|
||||
|
||||
if ((res = swr_init(d->swrContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to swr_init for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
d->maxDstSamples = d->srcSamples;
|
||||
if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_samples_alloc_array_and_samples for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
|
||||
|
||||
if ((res = avcodec_parameters_from_context(d->stream->codecpar, d->codecContext)) < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_parameters_from_context for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write file header
|
||||
if ((res = avformat_write_header(d->fmtContext, 0)) < 0) {
|
||||
LOG(("Audio Error: Unable to avformat_write_header for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
_timer.start(50);
|
||||
_captured.clear();
|
||||
_captured.reserve(AudioVoiceMsgBufferSize);
|
||||
DEBUG_LOG(("Audio Capture: started!"));
|
||||
}
|
||||
|
||||
void Instance::Inner::onStop(bool needResult) {
|
||||
if (!_timer.isActive()) return; // in onStop() already
|
||||
_timer.stop();
|
||||
|
||||
if (d->device) {
|
||||
alcCaptureStop(d->device);
|
||||
onTimeout(); // get last data
|
||||
}
|
||||
|
||||
// Write what is left
|
||||
if (!_captured.isEmpty()) {
|
||||
auto fadeSamples = kCaptureFadeInDuration * kCaptureFrequency / 1000;
|
||||
auto capturedSamples = static_cast<int>(_captured.size() / sizeof(short));
|
||||
if ((_captured.size() % sizeof(short)) || (d->fullSamples + capturedSamples < kCaptureFrequency) || (capturedSamples < fadeSamples)) {
|
||||
d->fullSamples = 0;
|
||||
d->dataPos = 0;
|
||||
d->data.clear();
|
||||
d->waveformMod = 0;
|
||||
d->waveformPeak = 0;
|
||||
d->waveform.clear();
|
||||
} else {
|
||||
float64 coef = 1. / fadeSamples, fadedFrom = 0;
|
||||
for (short *ptr = ((short*)_captured.data()) + capturedSamples, *end = ptr - fadeSamples; ptr != end; ++fadedFrom) {
|
||||
--ptr;
|
||||
*ptr = qRound(fadedFrom * coef * *ptr);
|
||||
}
|
||||
if (capturedSamples % d->srcSamples) {
|
||||
int32 s = _captured.size();
|
||||
_captured.resize(s + (d->srcSamples - (capturedSamples % d->srcSamples)) * sizeof(short));
|
||||
memset(_captured.data() + s, 0, _captured.size() - s);
|
||||
}
|
||||
|
||||
int32 framesize = d->srcSamples * d->codecContext->channels * sizeof(short), encoded = 0;
|
||||
while (_captured.size() >= encoded + framesize) {
|
||||
processFrame(encoded, framesize);
|
||||
encoded += framesize;
|
||||
}
|
||||
writeFrame(nullptr); // drain the codec
|
||||
if (encoded != _captured.size()) {
|
||||
d->fullSamples = 0;
|
||||
d->dataPos = 0;
|
||||
d->data.clear();
|
||||
d->waveformMod = 0;
|
||||
d->waveformPeak = 0;
|
||||
d->waveform.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
DEBUG_LOG(("Audio Capture: stopping (need result: %1), size: %2, samples: %3").arg(Logs::b(needResult)).arg(d->data.size()).arg(d->fullSamples));
|
||||
_captured = QByteArray();
|
||||
|
||||
// Finish stream
|
||||
if (d->device) {
|
||||
av_write_trailer(d->fmtContext);
|
||||
}
|
||||
|
||||
QByteArray result = d->fullSamples ? d->data : QByteArray();
|
||||
VoiceWaveform waveform;
|
||||
qint32 samples = d->fullSamples;
|
||||
if (samples && !d->waveform.isEmpty()) {
|
||||
int64 count = d->waveform.size(), sum = 0;
|
||||
if (count >= WaveformSamplesCount) {
|
||||
QVector<uint16> peaks;
|
||||
peaks.reserve(WaveformSamplesCount);
|
||||
|
||||
uint16 peak = 0;
|
||||
for (int32 i = 0; i < count; ++i) {
|
||||
uint16 sample = uint16(d->waveform.at(i)) * 256;
|
||||
if (peak < sample) {
|
||||
peak = sample;
|
||||
}
|
||||
sum += WaveformSamplesCount;
|
||||
if (sum >= count) {
|
||||
sum -= count;
|
||||
peaks.push_back(peak);
|
||||
peak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int64 sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0ULL);
|
||||
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 (d->device) {
|
||||
alcCaptureStop(d->device);
|
||||
alcCaptureCloseDevice(d->device);
|
||||
d->device = nullptr;
|
||||
|
||||
if (d->codecContext) {
|
||||
avcodec_free_context(&d->codecContext);
|
||||
d->codecContext = nullptr;
|
||||
}
|
||||
if (d->srcSamplesData) {
|
||||
if (d->srcSamplesData[0]) {
|
||||
av_freep(&d->srcSamplesData[0]);
|
||||
}
|
||||
av_freep(&d->srcSamplesData);
|
||||
}
|
||||
if (d->dstSamplesData) {
|
||||
if (d->dstSamplesData[0]) {
|
||||
av_freep(&d->dstSamplesData[0]);
|
||||
}
|
||||
av_freep(&d->dstSamplesData);
|
||||
}
|
||||
d->fullSamples = 0;
|
||||
if (d->swrContext) {
|
||||
swr_free(&d->swrContext);
|
||||
d->swrContext = nullptr;
|
||||
}
|
||||
if (d->opened) {
|
||||
avformat_close_input(&d->fmtContext);
|
||||
d->opened = false;
|
||||
}
|
||||
if (d->ioContext) {
|
||||
av_freep(&d->ioContext->buffer);
|
||||
av_freep(&d->ioContext);
|
||||
d->ioBuffer = nullptr;
|
||||
} else if (d->ioBuffer) {
|
||||
av_freep(&d->ioBuffer);
|
||||
}
|
||||
if (d->fmtContext) {
|
||||
avformat_free_context(d->fmtContext);
|
||||
d->fmtContext = nullptr;
|
||||
}
|
||||
d->fmt = nullptr;
|
||||
d->stream = nullptr;
|
||||
d->codec = nullptr;
|
||||
|
||||
d->lastUpdate = 0;
|
||||
d->levelMax = 0;
|
||||
|
||||
d->dataPos = 0;
|
||||
d->data.clear();
|
||||
|
||||
d->waveformMod = 0;
|
||||
d->waveformPeak = 0;
|
||||
d->waveform.clear();
|
||||
}
|
||||
if (needResult) emit done(result, waveform, samples);
|
||||
}
|
||||
|
||||
void Instance::Inner::onTimeout() {
|
||||
if (!d->device) {
|
||||
_timer.stop();
|
||||
return;
|
||||
}
|
||||
ALint samples;
|
||||
alcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, sizeof(samples), &samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
if (samples > 0) {
|
||||
// Get samples from OpenAL
|
||||
auto s = _captured.size();
|
||||
auto news = s + static_cast<int>(samples * sizeof(short));
|
||||
if (news / AudioVoiceMsgBufferSize > s / AudioVoiceMsgBufferSize) {
|
||||
_captured.reserve(((news / AudioVoiceMsgBufferSize) + 1) * AudioVoiceMsgBufferSize);
|
||||
}
|
||||
_captured.resize(news);
|
||||
alcCaptureSamples(d->device, (ALCvoid *)(_captured.data() + s), samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Count new recording level and update view
|
||||
auto skipSamples = kCaptureSkipDuration * kCaptureFrequency / 1000;
|
||||
auto fadeSamples = kCaptureFadeInDuration * kCaptureFrequency / 1000;
|
||||
auto levelindex = d->fullSamples + static_cast<int>(s / sizeof(short));
|
||||
for (auto ptr = (const short*)(_captured.constData() + s), end = (const short*)(_captured.constData() + news); ptr < end; ++ptr, ++levelindex) {
|
||||
if (levelindex > skipSamples) {
|
||||
uint16 value = qAbs(*ptr);
|
||||
if (levelindex < skipSamples + fadeSamples) {
|
||||
value = qRound(value * float64(levelindex - skipSamples) / fadeSamples);
|
||||
}
|
||||
if (d->levelMax < value) {
|
||||
d->levelMax = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
qint32 samplesFull = d->fullSamples + _captured.size() / sizeof(short), samplesSinceUpdate = samplesFull - d->lastUpdate;
|
||||
if (samplesSinceUpdate > AudioVoiceMsgUpdateView * kCaptureFrequency / 1000) {
|
||||
emit updated(d->levelMax, samplesFull);
|
||||
d->lastUpdate = samplesFull;
|
||||
d->levelMax = 0;
|
||||
}
|
||||
// Write frames
|
||||
int32 framesize = d->srcSamples * d->codecContext->channels * sizeof(short), encoded = 0;
|
||||
while (uint32(_captured.size()) >= encoded + framesize + fadeSamples * sizeof(short)) {
|
||||
processFrame(encoded, framesize);
|
||||
encoded += framesize;
|
||||
}
|
||||
|
||||
// Collapse the buffer
|
||||
if (encoded > 0) {
|
||||
int32 goodSize = _captured.size() - encoded;
|
||||
memmove(_captured.data(), _captured.constData() + encoded, goodSize);
|
||||
_captured.resize(goodSize);
|
||||
}
|
||||
} else {
|
||||
DEBUG_LOG(("Audio Capture: no samples to capture."));
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::processFrame(int32 offset, int32 framesize) {
|
||||
// Prepare audio frame
|
||||
|
||||
if (framesize % sizeof(short)) { // in the middle of a sample
|
||||
LOG(("Audio Error: Bad framesize in writeFrame() for capture, framesize %1, %2").arg(framesize));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
auto samplesCnt = static_cast<int>(framesize / sizeof(short));
|
||||
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
|
||||
auto srcSamplesDataChannel = (short*)(_captured.data() + offset);
|
||||
auto srcSamplesData = &srcSamplesDataChannel;
|
||||
|
||||
// memcpy(d->srcSamplesData[0], _captured.constData() + offset, framesize);
|
||||
auto skipSamples = static_cast<int>(kCaptureSkipDuration * kCaptureFrequency / 1000);
|
||||
auto fadeSamples = static_cast<int>(kCaptureFadeInDuration * kCaptureFrequency / 1000);
|
||||
if (d->fullSamples < skipSamples + fadeSamples) {
|
||||
int32 fadedCnt = qMin(samplesCnt, skipSamples + fadeSamples - d->fullSamples);
|
||||
float64 coef = 1. / fadeSamples, fadedFrom = d->fullSamples - skipSamples;
|
||||
short *ptr = srcSamplesDataChannel, *zeroEnd = ptr + qMin(samplesCnt, qMax(0, skipSamples - d->fullSamples)), *end = ptr + fadedCnt;
|
||||
for (; ptr != zeroEnd; ++ptr, ++fadedFrom) {
|
||||
*ptr = 0;
|
||||
}
|
||||
for (; ptr != end; ++ptr, ++fadedFrom) {
|
||||
*ptr = qRound(fadedFrom * coef * *ptr);
|
||||
}
|
||||
}
|
||||
|
||||
d->waveform.reserve(d->waveform.size() + (samplesCnt / d->waveformEach) + 1);
|
||||
for (short *ptr = srcSamplesDataChannel, *end = ptr + samplesCnt; ptr != end; ++ptr) {
|
||||
uint16 value = qAbs(*ptr);
|
||||
if (d->waveformPeak < value) {
|
||||
d->waveformPeak = value;
|
||||
}
|
||||
if (++d->waveformMod == d->waveformEach) {
|
||||
d->waveformMod -= d->waveformEach;
|
||||
d->waveform.push_back(uchar(d->waveformPeak / 256));
|
||||
d->waveformPeak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to final format
|
||||
|
||||
d->dstSamples = av_rescale_rnd(swr_get_delay(d->swrContext, d->codecContext->sample_rate) + d->srcSamples, d->codecContext->sample_rate, d->codecContext->sample_rate, AV_ROUND_UP);
|
||||
if (d->dstSamples > d->maxDstSamples) {
|
||||
d->maxDstSamples = d->dstSamples;
|
||||
av_freep(&d->dstSamplesData[0]);
|
||||
if ((res = av_samples_alloc(d->dstSamplesData, 0, d->codecContext->channels, d->dstSamples, d->codecContext->sample_fmt, 1)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_samples_alloc for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->codecContext->channels, d->maxDstSamples, d->codecContext->sample_fmt, 0);
|
||||
}
|
||||
|
||||
if ((res = swr_convert(d->swrContext, d->dstSamplesData, d->dstSamples, (const uint8_t **)srcSamplesData, d->srcSamples)) < 0) {
|
||||
LOG(("Audio Error: Unable to swr_convert for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
// Write audio frame
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
|
||||
frame->nb_samples = d->dstSamples;
|
||||
frame->pts = av_rescale_q(d->fullSamples, AVRational { 1, d->codecContext->sample_rate }, d->codecContext->time_base);
|
||||
|
||||
avcodec_fill_audio_frame(frame, d->codecContext->channels, d->codecContext->sample_fmt, d->dstSamplesData[0], d->dstSamplesSize, 0);
|
||||
|
||||
writeFrame(frame);
|
||||
|
||||
d->fullSamples += samplesCnt;
|
||||
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
void Instance::Inner::writeFrame(AVFrame *frame) {
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
|
||||
res = avcodec_send_frame(d->codecContext, frame);
|
||||
if (res == AVERROR(EAGAIN)) {
|
||||
int packetsWritten = writePackets();
|
||||
if (packetsWritten < 0) {
|
||||
if (frame && packetsWritten == AVERROR_EOF) {
|
||||
LOG(("Audio Error: EOF in packets received when EAGAIN was got in avcodec_send_frame()"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
}
|
||||
return;
|
||||
} else if (!packetsWritten) {
|
||||
LOG(("Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()"));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
res = avcodec_send_frame(d->codecContext, frame);
|
||||
}
|
||||
if (res < 0) {
|
||||
LOG(("Audio Error: Unable to avcodec_send_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frame) { // drain
|
||||
if ((res = writePackets()) != AVERROR_EOF) {
|
||||
LOG(("Audio Error: not EOF in packets received when draining the codec, result %1").arg(res));
|
||||
onStop(false);
|
||||
emit error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Instance::Inner::writePackets() {
|
||||
AVPacket pkt;
|
||||
memset(&pkt, 0, sizeof(pkt)); // data and size must be 0;
|
||||
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
|
||||
int written = 0;
|
||||
do {
|
||||
av_init_packet(&pkt);
|
||||
if ((res = avcodec_receive_packet(d->codecContext, &pkt)) < 0) {
|
||||
if (res == AVERROR(EAGAIN)) {
|
||||
return written;
|
||||
} else if (res == AVERROR_EOF) {
|
||||
return res;
|
||||
}
|
||||
LOG(("Audio Error: Unable to avcodec_receive_packet for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return res;
|
||||
}
|
||||
|
||||
av_packet_rescale_ts(&pkt, d->codecContext->time_base, d->stream->time_base);
|
||||
pkt.stream_index = d->stream->index;
|
||||
if ((res = av_interleaved_write_frame(d->fmtContext, &pkt)) < 0) {
|
||||
LOG(("Audio Error: Unable to av_interleaved_write_frame for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
onStop(false);
|
||||
emit error();
|
||||
return -1;
|
||||
}
|
||||
|
||||
++written;
|
||||
av_packet_unref(&pkt);
|
||||
} while (true);
|
||||
return written;
|
||||
}
|
||||
|
||||
} // namespace Capture
|
||||
} // namespace Media
|
||||
101
Telegram/SourceFiles/media/media_audio_capture.h
Normal file
101
Telegram/SourceFiles/media/media_audio_capture.h
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
struct AVFrame;
|
||||
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
|
||||
void Init();
|
||||
void DeInit();
|
||||
|
||||
class Instance : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Instance();
|
||||
|
||||
void check();
|
||||
bool available() const {
|
||||
return _available;
|
||||
}
|
||||
|
||||
~Instance();
|
||||
|
||||
signals:
|
||||
void start();
|
||||
void stop(bool needResult);
|
||||
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void error();
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
friend class Inner;
|
||||
|
||||
bool _available = false;
|
||||
QThread _thread;
|
||||
Inner *_inner;
|
||||
|
||||
};
|
||||
|
||||
Instance *instance();
|
||||
|
||||
class Instance::Inner : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Inner(QThread *thread);
|
||||
~Inner();
|
||||
|
||||
signals:
|
||||
void error();
|
||||
void updated(quint16 level, qint32 samples);
|
||||
void done(QByteArray data, VoiceWaveform waveform, qint32 samples);
|
||||
|
||||
public slots:
|
||||
void onInit();
|
||||
void onStart();
|
||||
void onStop(bool needResult);
|
||||
|
||||
void onTimeout();
|
||||
|
||||
private:
|
||||
void processFrame(int32 offset, int32 framesize);
|
||||
|
||||
void writeFrame(AVFrame *frame);
|
||||
|
||||
// Writes the packets till EAGAIN is got from av_receive_packet()
|
||||
// Returns number of packets written or -1 on error
|
||||
int writePackets();
|
||||
|
||||
struct Private;
|
||||
Private *d;
|
||||
QTimer _timer;
|
||||
QByteArray _captured;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Capture
|
||||
} // namespace Media
|
||||
|
||||
@@ -204,7 +204,7 @@ bool FFMpegLoader::open(qint64 &position) {
|
||||
int64_t src_ch_layout = layout, dst_ch_layout = AudioToChannelLayout;
|
||||
srcRate = freq;
|
||||
AVSampleFormat src_sample_fmt = inputFormat, dst_sample_fmt = AudioToFormat;
|
||||
dstRate = (freq != 44100 && freq != 48000) ? AudioVoiceMsgFrequency : freq;
|
||||
dstRate = (freq != 44100 && freq != 48000) ? Media::Player::kDefaultFrequency : freq;
|
||||
|
||||
av_opt_set_int(swrContext, "in_channel_layout", src_ch_layout, 0);
|
||||
av_opt_set_int(swrContext, "in_sample_rate", srcRate, 0);
|
||||
|
||||
@@ -20,6 +20,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/media_audio.h"
|
||||
#include "media/media_audio_loader.h"
|
||||
|
||||
extern "C" {
|
||||
@@ -49,7 +50,7 @@ public:
|
||||
~AbstractFFMpegLoader();
|
||||
|
||||
protected:
|
||||
int32 freq = AudioVoiceMsgFrequency;
|
||||
int32 freq = Media::Player::kDefaultFrequency;
|
||||
TimeMs len = 0;
|
||||
|
||||
uchar *ioBuffer = nullptr;
|
||||
@@ -89,8 +90,8 @@ private:
|
||||
ReadResult readFromReadyFrame(QByteArray &result, int64 &samplesAdded);
|
||||
|
||||
int32 fmt = AL_FORMAT_STEREO16;
|
||||
int32 srcRate = AudioVoiceMsgFrequency;
|
||||
int32 dstRate = AudioVoiceMsgFrequency;
|
||||
int32 srcRate = Media::Player::kDefaultFrequency;
|
||||
int32 dstRate = Media::Player::kDefaultFrequency;
|
||||
int32 maxResampleSamples = 1024;
|
||||
uint8_t **dstSamplesData = nullptr;
|
||||
|
||||
|
||||
@@ -25,13 +25,16 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "media/media_audio_ffmpeg_loader.h"
|
||||
#include "media/media_child_ffmpeg_loader.h"
|
||||
|
||||
AudioPlayerLoaders::AudioPlayerLoaders(QThread *thread) : _fromVideoNotify(this, "onVideoSoundAdded") {
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
Loaders::Loaders(QThread *thread) : _fromVideoNotify(this, "onVideoSoundAdded") {
|
||||
moveToThread(thread);
|
||||
connect(thread, SIGNAL(started()), this, SLOT(onInit()));
|
||||
connect(thread, SIGNAL(finished()), this, SLOT(deleteLater()));
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::feedFromVideo(VideoSoundPart &&part) {
|
||||
void Loaders::feedFromVideo(VideoSoundPart &&part) {
|
||||
bool invoke = false;
|
||||
{
|
||||
QMutexLocker lock(&_fromVideoMutex);
|
||||
@@ -47,17 +50,17 @@ void AudioPlayerLoaders::feedFromVideo(VideoSoundPart &&part) {
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::startFromVideo(uint64 videoPlayId) {
|
||||
void Loaders::startFromVideo(uint64 videoPlayId) {
|
||||
QMutexLocker lock(&_fromVideoMutex);
|
||||
_fromVideoPlayId = videoPlayId;
|
||||
clearFromVideoQueue();
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::stopFromVideo() {
|
||||
void Loaders::stopFromVideo() {
|
||||
startFromVideo(0);
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::onVideoSoundAdded() {
|
||||
void Loaders::onVideoSoundAdded() {
|
||||
bool waitingAndAdded = false;
|
||||
{
|
||||
QMutexLocker lock(&_fromVideoMutex);
|
||||
@@ -71,12 +74,12 @@ void AudioPlayerLoaders::onVideoSoundAdded() {
|
||||
}
|
||||
}
|
||||
|
||||
AudioPlayerLoaders::~AudioPlayerLoaders() {
|
||||
Loaders::~Loaders() {
|
||||
QMutexLocker lock(&_fromVideoMutex);
|
||||
clearFromVideoQueue();
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::clearFromVideoQueue() {
|
||||
void Loaders::clearFromVideoQueue() {
|
||||
auto queue = base::take(_fromVideoQueue);
|
||||
for (auto &packetData : queue) {
|
||||
AVPacket packet;
|
||||
@@ -85,27 +88,26 @@ void AudioPlayerLoaders::clearFromVideoQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::onInit() {
|
||||
void Loaders::onInit() {
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::onStart(const AudioMsgId &audio, qint64 position) {
|
||||
void Loaders::onStart(const AudioMsgId &audio, qint64 position) {
|
||||
auto type = audio.type();
|
||||
clear(type);
|
||||
{
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
AudioPlayer *voice = audioPlayer();
|
||||
if (!voice) return;
|
||||
if (!mixer()) return;
|
||||
|
||||
auto data = voice->dataForType(type);
|
||||
if (!data) return;
|
||||
auto track = mixer()->trackForType(type);
|
||||
if (!track) return;
|
||||
|
||||
data->loading = true;
|
||||
track->loading = true;
|
||||
}
|
||||
|
||||
loadData(audio, position);
|
||||
}
|
||||
|
||||
AudioMsgId AudioPlayerLoaders::clear(AudioMsgId::Type type) {
|
||||
AudioMsgId Loaders::clear(AudioMsgId::Type type) {
|
||||
AudioMsgId result;
|
||||
switch (type) {
|
||||
case AudioMsgId::Type::Voice: std::swap(result, _audio); _audioLoader = nullptr; break;
|
||||
@@ -115,23 +117,23 @@ AudioMsgId AudioPlayerLoaders::clear(AudioMsgId::Type type) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::setStoppedState(AudioPlayer::AudioMsg *m, AudioPlayerState state) {
|
||||
m->playbackState.state = state;
|
||||
m->playbackState.position = 0;
|
||||
void Loaders::setStoppedState(Mixer::Track *track, State state) {
|
||||
track->state.state = state;
|
||||
track->state.position = 0;
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::emitError(AudioMsgId::Type type) {
|
||||
void Loaders::emitError(AudioMsgId::Type type) {
|
||||
emit error(clear(type));
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::onLoad(const AudioMsgId &audio) {
|
||||
void Loaders::onLoad(const AudioMsgId &audio) {
|
||||
loadData(audio, 0);
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
SetupError err = SetupNoErrorStarted;
|
||||
void Loaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
auto err = SetupNoErrorStarted;
|
||||
auto type = audio.type();
|
||||
AudioPlayerLoader *l = setupLoader(audio, err, position);
|
||||
auto l = setupLoader(audio, err, position);
|
||||
if (!l) {
|
||||
if (err == SetupErrorAtStart) {
|
||||
emitError(type);
|
||||
@@ -139,10 +141,10 @@ void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool started = (err == SetupNoErrorStarted);
|
||||
bool finished = false;
|
||||
bool waiting = false;
|
||||
bool errAtStart = started;
|
||||
auto started = (err == SetupNoErrorStarted);
|
||||
auto finished = false;
|
||||
auto waiting = false;
|
||||
auto errAtStart = started;
|
||||
|
||||
QByteArray samples;
|
||||
int64 samplesCount = 0;
|
||||
@@ -156,8 +158,9 @@ void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
if (errAtStart) {
|
||||
{
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
AudioPlayer::AudioMsg *m = checkLoader(type);
|
||||
if (m) m->playbackState.state = AudioPlayerStoppedAtStart;
|
||||
if (auto track = checkLoader(type)) {
|
||||
track->state.state = State::StoppedAtStart;
|
||||
}
|
||||
}
|
||||
emitError(type);
|
||||
return;
|
||||
@@ -185,93 +188,54 @@ void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
}
|
||||
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
AudioPlayer::AudioMsg *m = checkLoader(type);
|
||||
if (!m) {
|
||||
auto track = checkLoader(type);
|
||||
if (!track) {
|
||||
clear(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (started) {
|
||||
if (m->source) {
|
||||
alSourceStop(m->source);
|
||||
for (int32 i = 0; i < 3; ++i) {
|
||||
if (m->samplesCount[i]) {
|
||||
ALuint buffer = 0;
|
||||
alSourceUnqueueBuffers(m->source, 1, &buffer);
|
||||
m->samplesCount[i] = 0;
|
||||
}
|
||||
}
|
||||
m->nextBuffer = 0;
|
||||
mixer()->reattachTracks();
|
||||
|
||||
track->started();
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtStart);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
m->skipStart = position;
|
||||
m->skipEnd = m->playbackState.duration - position;
|
||||
m->playbackState.position = position;
|
||||
m->started = 0;
|
||||
|
||||
track->bufferedPosition = position;
|
||||
track->state.position = position;
|
||||
track->fadeStartPosition = position;
|
||||
|
||||
track->format = l->format();
|
||||
track->frequency = l->frequency();
|
||||
}
|
||||
if (samplesCount) {
|
||||
if (!m->source) {
|
||||
alGenSources(1, &m->source);
|
||||
alSourcef(m->source, AL_PITCH, 1.f);
|
||||
alSource3f(m->source, AL_POSITION, 0, 0, 0);
|
||||
alSource3f(m->source, AL_VELOCITY, 0, 0, 0);
|
||||
alSourcei(m->source, AL_LOOPING, 0);
|
||||
}
|
||||
if (!m->buffers[m->nextBuffer]) {
|
||||
alGenBuffers(3, m->buffers);
|
||||
}
|
||||
track->ensureStreamCreated();
|
||||
|
||||
// If this buffer is queued, try to unqueue some buffer.
|
||||
if (m->samplesCount[m->nextBuffer]) {
|
||||
ALint processed = 0;
|
||||
alGetSourcei(m->source, AL_BUFFERS_PROCESSED, &processed);
|
||||
if (processed < 1) { // No processed buffers, wait.
|
||||
l->saveDecodedSamples(&samples, &samplesCount);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unqueue some processed buffer.
|
||||
ALuint buffer = 0;
|
||||
alSourceUnqueueBuffers(m->source, 1, &buffer);
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find it in the list and make it the nextBuffer.
|
||||
bool found = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (m->buffers[i] == buffer) {
|
||||
found = true;
|
||||
m->nextBuffer = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
LOG(("Audio Error: Could not find the unqueued buffer! Buffer %1 in source %2 with processed count %3").arg(buffer).arg(m->source).arg(processed));
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m->samplesCount[m->nextBuffer]) {
|
||||
m->skipStart += m->samplesCount[m->nextBuffer];
|
||||
m->samplesCount[m->nextBuffer] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto frequency = l->frequency();
|
||||
auto format = l->format();
|
||||
m->samplesCount[m->nextBuffer] = samplesCount;
|
||||
alBufferData(m->buffers[m->nextBuffer], format, samples.constData(), samples.size(), frequency);
|
||||
|
||||
alSourceQueueBuffers(m->source, 1, m->buffers + m->nextBuffer);
|
||||
m->skipEnd -= samplesCount;
|
||||
|
||||
m->nextBuffer = (m->nextBuffer + 1) % 3;
|
||||
auto bufferIndex = track->getNotQueuedBufferIndex();
|
||||
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bufferIndex < 0) { // No free buffers, wait.
|
||||
l->saveDecodedSamples(&samples, &samplesCount);
|
||||
return;
|
||||
}
|
||||
|
||||
track->bufferSamples[bufferIndex] = samples;
|
||||
track->samplesCount[bufferIndex] = samplesCount;
|
||||
track->bufferedLength += samplesCount;
|
||||
alBufferData(track->stream.buffers[bufferIndex], track->format, samples.constData(), samples.size(), track->frequency);
|
||||
|
||||
alSourceQueueBuffers(track->stream.source, 1, track->stream.buffers + bufferIndex);
|
||||
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
@@ -283,33 +247,31 @@ void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
m->skipEnd = 0;
|
||||
m->playbackState.duration = m->skipStart + m->samplesCount[0] + m->samplesCount[1] + m->samplesCount[2];
|
||||
track->loaded = true;
|
||||
track->state.duration = track->bufferedPosition + track->bufferedLength;
|
||||
clear(type);
|
||||
}
|
||||
|
||||
m->loading = false;
|
||||
if (m->playbackState.state == AudioPlayerResuming || m->playbackState.state == AudioPlayerPlaying || m->playbackState.state == AudioPlayerStarting) {
|
||||
track->loading = false;
|
||||
if (track->state.state == State::Resuming || track->state.state == State::Playing || track->state.state == State::Starting) {
|
||||
ALint state = AL_INITIAL;
|
||||
alGetSourcei(m->source, AL_SOURCE_STATE, &state);
|
||||
alGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);
|
||||
if (internal::audioCheckError()) {
|
||||
if (state != AL_PLAYING) {
|
||||
audioPlayer()->resumeDevice();
|
||||
|
||||
switch (type) {
|
||||
case AudioMsgId::Type::Voice: alSourcef(m->source, AL_GAIN, internal::audioSuppressGain()); break;
|
||||
case AudioMsgId::Type::Song: alSourcef(m->source, AL_GAIN, internal::audioSuppressSongGain() * Global::SongVolume()); break;
|
||||
case AudioMsgId::Type::Video: alSourcef(m->source, AL_GAIN, internal::audioSuppressSongGain() * Global::VideoVolume()); break;
|
||||
if (state == AL_STOPPED && !internal::CheckAudioDeviceConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
alSourcef(track->stream.source, AL_GAIN, ComputeVolume(type));
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
alSourcePlay(m->source);
|
||||
alSourcePlay(track->stream.source);
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
@@ -317,20 +279,19 @@ void AudioPlayerLoaders::loadData(AudioMsgId audio, qint64 position) {
|
||||
emit needToCheck();
|
||||
}
|
||||
} else {
|
||||
setStoppedState(m, AudioPlayerStoppedAtError);
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AudioPlayerLoader *AudioPlayerLoaders::setupLoader(const AudioMsgId &audio, SetupError &err, qint64 &position) {
|
||||
AudioPlayerLoader *Loaders::setupLoader(const AudioMsgId &audio, SetupError &err, qint64 &position) {
|
||||
err = SetupErrorAtStart;
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
AudioPlayer *voice = audioPlayer();
|
||||
if (!voice) return nullptr;
|
||||
if (!mixer()) return nullptr;
|
||||
|
||||
auto data = voice->dataForType(audio.type());
|
||||
if (!data || data->audio != audio || !data->loading) {
|
||||
auto track = mixer()->trackForType(audio.type());
|
||||
if (!track || track->state.id != audio || !track->loading) {
|
||||
emit error(audio);
|
||||
LOG(("Audio Error: trying to load part of audio, that is not current at the moment"));
|
||||
err = SetupErrorNotPlaying;
|
||||
@@ -345,7 +306,7 @@ AudioPlayerLoader *AudioPlayerLoaders::setupLoader(const AudioMsgId &audio, Setu
|
||||
case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (_video == audio); break;
|
||||
}
|
||||
|
||||
if (l && (!isGoodId || !l->check(data->file, data->data))) {
|
||||
if (l && (!isGoodId || !l->check(track->file, track->data))) {
|
||||
clear(audio.type());
|
||||
l = nullptr;
|
||||
}
|
||||
@@ -359,65 +320,62 @@ AudioPlayerLoader *AudioPlayerLoaders::setupLoader(const AudioMsgId &audio, Setu
|
||||
}
|
||||
|
||||
if (audio.type() == AudioMsgId::Type::Video) {
|
||||
if (!data->videoData) {
|
||||
data->playbackState.state = AudioPlayerStoppedAtError;
|
||||
if (!track->videoData) {
|
||||
track->state.state = State::StoppedAtError;
|
||||
emit error(audio);
|
||||
LOG(("Audio Error: video sound data not ready"));
|
||||
return nullptr;
|
||||
}
|
||||
_videoLoader = std_::make_unique<ChildFFMpegLoader>(data->videoPlayId, std_::move(data->videoData));
|
||||
_videoLoader = std_::make_unique<ChildFFMpegLoader>(track->videoPlayId, std_::move(track->videoData));
|
||||
l = _videoLoader.get();
|
||||
} else {
|
||||
*loader = std_::make_unique<FFMpegLoader>(data->file, data->data);
|
||||
*loader = std_::make_unique<FFMpegLoader>(track->file, track->data);
|
||||
l = loader->get();
|
||||
}
|
||||
|
||||
if (!l->open(position)) {
|
||||
data->playbackState.state = AudioPlayerStoppedAtStart;
|
||||
track->state.state = State::StoppedAtStart;
|
||||
return nullptr;
|
||||
}
|
||||
int64 duration = l->duration();
|
||||
if (duration <= 0) {
|
||||
data->playbackState.state = AudioPlayerStoppedAtStart;
|
||||
track->state.state = State::StoppedAtStart;
|
||||
return nullptr;
|
||||
}
|
||||
data->playbackState.duration = duration;
|
||||
data->playbackState.frequency = l->frequency();
|
||||
if (!data->playbackState.frequency) data->playbackState.frequency = AudioVoiceMsgFrequency;
|
||||
track->state.duration = duration;
|
||||
track->state.frequency = l->frequency();
|
||||
if (!track->state.frequency) track->state.frequency = kDefaultFrequency;
|
||||
err = SetupNoErrorStarted;
|
||||
} else {
|
||||
if (!data->skipEnd) {
|
||||
err = SetupErrorLoadedFull;
|
||||
LOG(("Audio Error: trying to load part of audio, that is already loaded to the end"));
|
||||
return nullptr;
|
||||
}
|
||||
} else if (track->loaded) {
|
||||
err = SetupErrorLoadedFull;
|
||||
LOG(("Audio Error: trying to load part of audio, that is already loaded to the end"));
|
||||
return nullptr;
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
AudioPlayer::AudioMsg *AudioPlayerLoaders::checkLoader(AudioMsgId::Type type) {
|
||||
AudioPlayer *voice = audioPlayer();
|
||||
if (!voice) return 0;
|
||||
Mixer::Track *Loaders::checkLoader(AudioMsgId::Type type) {
|
||||
if (!mixer()) return nullptr;
|
||||
|
||||
auto data = voice->dataForType(type);
|
||||
bool isGoodId = false;
|
||||
auto track = mixer()->trackForType(type);
|
||||
auto isGoodId = false;
|
||||
AudioPlayerLoader *l = nullptr;
|
||||
switch (type) {
|
||||
case AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (data->audio == _audio); break;
|
||||
case AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (data->audio == _song); break;
|
||||
case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (data->audio == _video); break;
|
||||
case AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (track->state.id == _audio); break;
|
||||
case AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (track->state.id == _song); break;
|
||||
case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (track->state.id == _video); break;
|
||||
}
|
||||
if (!l || !data) return nullptr;
|
||||
if (!l || !track) return nullptr;
|
||||
|
||||
if (!isGoodId || !data->loading || !l->check(data->file, data->data)) {
|
||||
if (!isGoodId || !track->loading || !l->check(track->file, track->data)) {
|
||||
LOG(("Audio Error: playing changed while loading"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
return track;
|
||||
}
|
||||
|
||||
void AudioPlayerLoaders::onCancel(const AudioMsgId &audio) {
|
||||
void Loaders::onCancel(const AudioMsgId &audio) {
|
||||
switch (audio.type()) {
|
||||
case AudioMsgId::Type::Voice: if (_audio == audio) clear(audio.type()); break;
|
||||
case AudioMsgId::Type::Song: if (_song == audio) clear(audio.type()); break;
|
||||
@@ -425,13 +383,15 @@ void AudioPlayerLoaders::onCancel(const AudioMsgId &audio) {
|
||||
}
|
||||
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
AudioPlayer *voice = audioPlayer();
|
||||
if (!voice) return;
|
||||
if (!mixer()) return;
|
||||
|
||||
for (int i = 0; i < AudioSimultaneousLimit; ++i) {
|
||||
auto data = voice->dataForType(audio.type(), i);
|
||||
if (data->audio == audio) {
|
||||
data->loading = false;
|
||||
for (auto i = 0; i != kTogetherLimit; ++i) {
|
||||
auto track = mixer()->trackForType(audio.type(), i);
|
||||
if (track->state.id == audio) {
|
||||
track->loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
@@ -26,21 +26,25 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
|
||||
class AudioPlayerLoader;
|
||||
class ChildFFMpegLoader;
|
||||
class AudioPlayerLoaders : public QObject {
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
class Loaders : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AudioPlayerLoaders(QThread *thread);
|
||||
Loaders(QThread *thread);
|
||||
void startFromVideo(uint64 videoPlayId);
|
||||
void stopFromVideo();
|
||||
void feedFromVideo(VideoSoundPart &&part);
|
||||
~AudioPlayerLoaders();
|
||||
~Loaders();
|
||||
|
||||
signals:
|
||||
void error(const AudioMsgId &audio);
|
||||
void needToCheck();
|
||||
|
||||
public slots:
|
||||
public slots:
|
||||
void onInit();
|
||||
|
||||
void onStart(const AudioMsgId &audio, qint64 position);
|
||||
@@ -64,7 +68,7 @@ private:
|
||||
|
||||
void emitError(AudioMsgId::Type type);
|
||||
AudioMsgId clear(AudioMsgId::Type type);
|
||||
void setStoppedState(AudioPlayer::AudioMsg *m, AudioPlayerState state = AudioPlayerStopped);
|
||||
void setStoppedState(Mixer::Track *m, State state = State::Stopped);
|
||||
|
||||
enum SetupError {
|
||||
SetupErrorAtStart = 0,
|
||||
@@ -74,6 +78,9 @@ private:
|
||||
};
|
||||
void loadData(AudioMsgId audio, qint64 position);
|
||||
AudioPlayerLoader *setupLoader(const AudioMsgId &audio, SetupError &err, qint64 &position);
|
||||
AudioPlayer::AudioMsg *checkLoader(AudioMsgId::Type type);
|
||||
Mixer::Track *checkLoader(AudioMsgId::Type type);
|
||||
|
||||
};
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
@@ -83,7 +83,7 @@ bool ChildFFMpegLoader::open(qint64 &position) {
|
||||
int64_t src_ch_layout = layout, dst_ch_layout = AudioToChannelLayout;
|
||||
_srcRate = _parentData->frequency;
|
||||
AVSampleFormat src_sample_fmt = _inputFormat, dst_sample_fmt = AudioToFormat;
|
||||
_dstRate = (_parentData->frequency != 44100 && _parentData->frequency != 48000) ? AudioVoiceMsgFrequency : _parentData->frequency;
|
||||
_dstRate = (_parentData->frequency != 44100 && _parentData->frequency != 48000) ? Media::Player::kDefaultFrequency : _parentData->frequency;
|
||||
|
||||
av_opt_set_int(_swrContext, "in_channel_layout", src_ch_layout, 0);
|
||||
av_opt_set_int(_swrContext, "in_sample_rate", _srcRate, 0);
|
||||
|
||||
@@ -21,6 +21,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#pragma once
|
||||
|
||||
#include "media/media_audio_loader.h"
|
||||
#include "media/media_audio.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
@@ -33,7 +34,7 @@ extern "C" {
|
||||
|
||||
struct VideoSoundData {
|
||||
AVCodecContext *context = nullptr;
|
||||
int32 frequency = AudioVoiceMsgFrequency;
|
||||
int32 frequency = Media::Player::kDefaultFrequency;
|
||||
TimeMs length = 0;
|
||||
~VideoSoundData();
|
||||
};
|
||||
@@ -120,8 +121,8 @@ private:
|
||||
|
||||
int32 _sampleSize = 2 * sizeof(uint16);
|
||||
int32 _format = AL_FORMAT_STEREO16;
|
||||
int32 _srcRate = AudioVoiceMsgFrequency;
|
||||
int32 _dstRate = AudioVoiceMsgFrequency;
|
||||
int32 _srcRate = Media::Player::kDefaultFrequency;
|
||||
int32 _dstRate = Media::Player::kDefaultFrequency;
|
||||
int32 _maxResampleSamples = 1024;
|
||||
uint8_t **_dstSamplesData = nullptr;
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ ReaderImplementation::ReadResult FFMpegReaderImplementation::readFramesTill(Time
|
||||
}
|
||||
|
||||
// sync by audio stream
|
||||
auto correctMs = (frameMs >= 0) ? audioPlayer()->getVideoCorrectedTime(_playId, frameMs, systemMs) : frameMs;
|
||||
auto correctMs = (frameMs >= 0) ? Player::mixer()->getVideoCorrectedTime(_playId, frameMs, systemMs) : frameMs;
|
||||
if (!_frameRead) {
|
||||
auto readResult = readNextFrame();
|
||||
if (readResult != ReadResult::Success) {
|
||||
@@ -221,13 +221,13 @@ TimeMs FFMpegReaderImplementation::durationMs() const {
|
||||
|
||||
void FFMpegReaderImplementation::pauseAudio() {
|
||||
if (_audioStreamId >= 0) {
|
||||
audioPlayer()->pauseFromVideo(_playId);
|
||||
Player::mixer()->pauseFromVideo(_playId);
|
||||
}
|
||||
}
|
||||
|
||||
void FFMpegReaderImplementation::resumeAudio() {
|
||||
if (_audioStreamId >= 0) {
|
||||
audioPlayer()->resumeFromVideo(_playId);
|
||||
Player::mixer()->resumeFromVideo(_playId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ bool FFMpegReaderImplementation::start(Mode mode, TimeMs &positionMs) {
|
||||
_audioStreamId = av_find_best_stream(_fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
|
||||
if (_mode == Mode::OnlyGifv) {
|
||||
if (_audioStreamId >= 0) { // should be no audio stream
|
||||
_audioStreamId = -1; // do not attempt to access audioPlayer()
|
||||
_audioStreamId = -1;
|
||||
return false;
|
||||
}
|
||||
if (dataSize() > AnimationInMemory) {
|
||||
@@ -380,7 +380,7 @@ bool FFMpegReaderImplementation::start(Mode mode, TimeMs &positionMs) {
|
||||
if (_codecContext->codec_id != AV_CODEC_ID_H264) {
|
||||
return false;
|
||||
}
|
||||
} else if (_mode == Mode::Silent || !audioPlayer() || !_playId) {
|
||||
} else if (_mode == Mode::Silent || !_playId) {
|
||||
_audioStreamId = -1;
|
||||
}
|
||||
|
||||
@@ -436,8 +436,8 @@ bool FFMpegReaderImplementation::start(Mode mode, TimeMs &positionMs) {
|
||||
}
|
||||
|
||||
if (_audioStreamId >= 0) {
|
||||
int64 position = (positionMs * soundData->frequency) / 1000LL;
|
||||
audioPlayer()->initFromVideo(_playId, std_::move(soundData), position);
|
||||
auto position = (positionMs * soundData->frequency) / 1000LL;
|
||||
Player::mixer()->initFromVideo(_playId, std_::move(soundData), position);
|
||||
}
|
||||
|
||||
if (readResult == PacketResult::Ok) {
|
||||
@@ -453,7 +453,7 @@ QString FFMpegReaderImplementation::logData() const {
|
||||
|
||||
FFMpegReaderImplementation::~FFMpegReaderImplementation() {
|
||||
if (_audioStreamId >= 0) {
|
||||
audioPlayer()->stopFromVideo(_playId);
|
||||
Player::mixer()->stopFromVideo(_playId);
|
||||
}
|
||||
|
||||
clearPacketQueue();
|
||||
@@ -490,7 +490,7 @@ FFMpegReaderImplementation::PacketResult FFMpegReaderImplementation::readPacket(
|
||||
VideoSoundPart part;
|
||||
part.packet = &_packetNull;
|
||||
part.videoPlayId = _playId;
|
||||
audioPlayer()->feedFromVideo(std_::move(part));
|
||||
Player::mixer()->feedFromVideo(std_::move(part));
|
||||
}
|
||||
return PacketResult::EndOfFile;
|
||||
}
|
||||
@@ -516,7 +516,7 @@ void FFMpegReaderImplementation::processPacket(AVPacket *packet) {
|
||||
VideoSoundPart part;
|
||||
part.packet = packet;
|
||||
part.videoPlayId = _playId;
|
||||
audioPlayer()->feedFromVideo(std_::move(part));
|
||||
Player::mixer()->feedFromVideo(std_::move(part));
|
||||
}
|
||||
} else {
|
||||
av_packet_unref(packet);
|
||||
|
||||
@@ -35,13 +35,13 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
using State = PlayButtonLayout::State;
|
||||
using ButtonState = PlayButtonLayout::State;
|
||||
|
||||
class CoverWidget::PlayButton : public Ui::AbstractButton {
|
||||
public:
|
||||
PlayButton(QWidget *parent);
|
||||
|
||||
void setState(PlayButtonLayout::State state) {
|
||||
void setState(ButtonState state) {
|
||||
_layout.setState(state);
|
||||
}
|
||||
void finishTransform() {
|
||||
@@ -94,9 +94,7 @@ CoverWidget::CoverWidget(QWidget *parent) : TWidget(parent)
|
||||
handleSeekFinished(value);
|
||||
});
|
||||
_playPause->setClickedCallback([this] {
|
||||
if (exists()) {
|
||||
instance()->playPauseCancelClicked();
|
||||
}
|
||||
instance()->playPauseCancelClicked();
|
||||
});
|
||||
|
||||
updateRepeatTrackIcon();
|
||||
@@ -110,27 +108,22 @@ CoverWidget::CoverWidget(QWidget *parent) : TWidget(parent)
|
||||
Global::RefSongVolumeChanged().notify();
|
||||
});
|
||||
subscribe(Global::RefSongVolumeChanged(), [this] { updateVolumeToggleIcon(); });
|
||||
if (exists()) {
|
||||
subscribe(instance()->repeatChangedNotifier(), [this] {
|
||||
updateRepeatTrackIcon();
|
||||
});
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] {
|
||||
handlePlaylistUpdate();
|
||||
});
|
||||
subscribe(instance()->updatedNotifier(), [this](const UpdatedEvent &e) {
|
||||
handleSongUpdate(e);
|
||||
});
|
||||
subscribe(instance()->songChangedNotifier(), [this] {
|
||||
handleSongChange();
|
||||
});
|
||||
subscribe(instance()->repeatChangedNotifier(), [this] {
|
||||
updateRepeatTrackIcon();
|
||||
});
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] {
|
||||
handlePlaylistUpdate();
|
||||
});
|
||||
subscribe(instance()->updatedNotifier(), [this](const TrackState &state) {
|
||||
handleSongUpdate(state);
|
||||
});
|
||||
subscribe(instance()->songChangedNotifier(), [this] {
|
||||
handleSongChange();
|
||||
if (auto player = audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = player->currentState(&playing, AudioMsgId::Type::Song);
|
||||
handleSongUpdate(UpdatedEvent(&playing, &playbackState));
|
||||
_playPause->finishTransform();
|
||||
}
|
||||
}
|
||||
});
|
||||
handleSongChange();
|
||||
|
||||
handleSongUpdate(mixer()->currentState(AudioMsgId::Type::Song));
|
||||
_playPause->finishTransform();
|
||||
}
|
||||
|
||||
void CoverWidget::setPinCallback(ButtonCallback &&callback) {
|
||||
@@ -148,9 +141,7 @@ void CoverWidget::handleSeekProgress(float64 progress) {
|
||||
if (_seekPositionMs != positionMs) {
|
||||
_seekPositionMs = positionMs;
|
||||
updateTimeLabel();
|
||||
if (exists()) {
|
||||
instance()->startSeeking();
|
||||
}
|
||||
instance()->startSeeking();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,15 +151,13 @@ void CoverWidget::handleSeekFinished(float64 progress) {
|
||||
auto positionMs = snap(static_cast<TimeMs>(progress * _lastDurationMs), 0LL, _lastDurationMs);
|
||||
_seekPositionMs = -1;
|
||||
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing && playbackState.duration) {
|
||||
audioPlayer()->seek(qRound(progress * playbackState.duration));
|
||||
auto type = AudioMsgId::Type::Song;
|
||||
auto state = Media::Player::mixer()->currentState(type);
|
||||
if (state.id && state.duration) {
|
||||
Media::Player::mixer()->seek(type, qRound(progress * state.duration));
|
||||
}
|
||||
|
||||
if (exists()) {
|
||||
instance()->stopSeeking();
|
||||
}
|
||||
instance()->stopSeeking();
|
||||
}
|
||||
|
||||
void CoverWidget::resizeEvent(QResizeEvent *e) {
|
||||
@@ -237,52 +226,50 @@ void CoverWidget::updateRepeatTrackIcon() {
|
||||
_repeatTrack->setIconOverride(instance()->repeatEnabled() ? nullptr : &st::mediaPlayerRepeatInactiveIcon);
|
||||
}
|
||||
|
||||
void CoverWidget::handleSongUpdate(const UpdatedEvent &e) {
|
||||
auto &audioId = *e.audioId;
|
||||
auto &playbackState = *e.playbackState;
|
||||
if (!audioId || !audioId.audio()->song()) {
|
||||
void CoverWidget::handleSongUpdate(const TrackState &state) {
|
||||
if (!state.id || !state.id.audio()->song()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioId.audio()->loading()) {
|
||||
_playback->updateLoadingState(audioId.audio()->progress());
|
||||
if (state.id.audio()->loading()) {
|
||||
_playback->updateLoadingState(state.id.audio()->progress());
|
||||
} else {
|
||||
_playback->updateState(*e.playbackState);
|
||||
_playback->updateState(state);
|
||||
}
|
||||
|
||||
auto stopped = ((playbackState.state & AudioPlayerStoppedMask) || playbackState.state == AudioPlayerFinishing);
|
||||
auto showPause = !stopped && (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
if (exists() && instance()->isSeeking()) {
|
||||
auto stopped = (IsStopped(state.state) || state.state == State::Finishing);
|
||||
auto showPause = !stopped && (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
if (instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
auto state = [audio = audioId.audio(), showPause] {
|
||||
auto buttonState = [audio = state.id.audio(), showPause] {
|
||||
if (audio->loading()) {
|
||||
return State::Cancel;
|
||||
return ButtonState::Cancel;
|
||||
} else if (showPause) {
|
||||
return State::Pause;
|
||||
return ButtonState::Pause;
|
||||
}
|
||||
return State::Play;
|
||||
return ButtonState::Play;
|
||||
};
|
||||
_playPause->setState(state());
|
||||
_playPause->setState(buttonState());
|
||||
|
||||
updateTimeText(audioId, playbackState);
|
||||
updateTimeText(state);
|
||||
}
|
||||
|
||||
void CoverWidget::updateTimeText(const AudioMsgId &audioId, const AudioPlaybackState &playbackState) {
|
||||
void CoverWidget::updateTimeText(const TrackState &state) {
|
||||
QString time;
|
||||
qint64 position = 0, duration = 0, display = 0;
|
||||
auto frequency = (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
if (!(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
display = position = playbackState.position;
|
||||
duration = playbackState.duration;
|
||||
auto frequency = state.frequency;
|
||||
if (!IsStopped(state.state) && state.state != State::Finishing) {
|
||||
display = position = state.position;
|
||||
duration = state.duration;
|
||||
} else {
|
||||
display = playbackState.duration ? playbackState.duration : (audioId.audio()->song()->duration * frequency);
|
||||
display = state.duration ? state.duration : (state.id.audio()->song()->duration * frequency);
|
||||
}
|
||||
|
||||
_lastDurationMs = (playbackState.duration * 1000LL) / frequency;
|
||||
_lastDurationMs = (state.duration * 1000LL) / frequency;
|
||||
|
||||
if (audioId.audio()->loading()) {
|
||||
_time = QString::number(qRound(audioId.audio()->progress() * 100)) + '%';
|
||||
if (state.id.audio()->loading()) {
|
||||
_time = QString::number(qRound(state.id.audio()->progress() * 100)) + '%';
|
||||
_playback->setDisabled(true);
|
||||
} else {
|
||||
display = display / frequency;
|
||||
@@ -350,16 +337,12 @@ void CoverWidget::createPrevNextButtons() {
|
||||
_previousTrack.create(this, st::mediaPlayerPanelPreviousButton);
|
||||
_previousTrack->show();
|
||||
_previousTrack->setClickedCallback([this]() {
|
||||
if (exists()) {
|
||||
instance()->previous();
|
||||
}
|
||||
instance()->previous();
|
||||
});
|
||||
_nextTrack.create(this, st::mediaPlayerPanelNextButton);
|
||||
_nextTrack->show();
|
||||
_nextTrack->setClickedCallback([this]() {
|
||||
if (exists()) {
|
||||
instance()->next();
|
||||
}
|
||||
instance()->next();
|
||||
});
|
||||
updatePlayPrevNextPositions();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#pragma once
|
||||
|
||||
class AudioMsgId;
|
||||
struct AudioPlaybackState;
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
@@ -37,7 +36,7 @@ class Playback;
|
||||
namespace Player {
|
||||
|
||||
class VolumeController;
|
||||
struct UpdatedEvent;
|
||||
struct TrackState;
|
||||
|
||||
class CoverWidget : public TWidget, private base::Subscriber {
|
||||
public:
|
||||
@@ -66,11 +65,11 @@ private:
|
||||
|
||||
void updateVolumeToggleIcon();
|
||||
|
||||
void handleSongUpdate(const UpdatedEvent &e);
|
||||
void handleSongUpdate(const TrackState &state);
|
||||
void handleSongChange();
|
||||
void handlePlaylistUpdate();
|
||||
|
||||
void updateTimeText(const AudioMsgId &audioId, const AudioPlaybackState &playbackState);
|
||||
void updateTimeText(const TrackState &state);
|
||||
void updateTimeLabel();
|
||||
|
||||
TimeMs _seekPositionMs = -1;
|
||||
|
||||
@@ -22,6 +22,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "media/player/media_player_instance.h"
|
||||
|
||||
#include "media/media_audio.h"
|
||||
#include "media/media_audio_capture.h"
|
||||
#include "observer_peer.h"
|
||||
|
||||
namespace Media {
|
||||
@@ -33,24 +34,21 @@ Instance *SingleInstance = nullptr;
|
||||
} // namespace
|
||||
|
||||
void start() {
|
||||
audioInit();
|
||||
if (audioPlayer()) {
|
||||
SingleInstance = new Instance();
|
||||
}
|
||||
}
|
||||
InitAudio();
|
||||
Capture::Init();
|
||||
|
||||
bool exists() {
|
||||
return (audioPlayer() != nullptr);
|
||||
SingleInstance = new Instance();
|
||||
}
|
||||
|
||||
void finish() {
|
||||
delete base::take(SingleInstance);
|
||||
|
||||
audioFinish();
|
||||
Capture::DeInit();
|
||||
DeInitAudio();
|
||||
}
|
||||
|
||||
Instance::Instance() {
|
||||
subscribe(audioPlayer(), [this](const AudioMsgId &audioId) {
|
||||
subscribe(Media::Player::Updated(), [this](const AudioMsgId &audioId) {
|
||||
if (audioId.type() == AudioMsgId::Type::Song) {
|
||||
handleSongUpdate(audioId);
|
||||
}
|
||||
@@ -152,18 +150,15 @@ Instance *instance() {
|
||||
}
|
||||
|
||||
void Instance::play() {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing) {
|
||||
if (playbackState.state & AudioPlayerStoppedMask) {
|
||||
audioPlayer()->play(playing);
|
||||
} else {
|
||||
if (playbackState.state == AudioPlayerPausing || playbackState.state == AudioPlayerPaused || playbackState.state == AudioPlayerPausedAtEnd) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Song);
|
||||
}
|
||||
auto state = mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id) {
|
||||
if (IsStopped(state.state)) {
|
||||
mixer()->play(state.id);
|
||||
} else if (IsPaused(state.state) || state.state == State::Pausing) {
|
||||
mixer()->pauseresume(AudioMsgId::Type::Song);
|
||||
}
|
||||
} else if (_current) {
|
||||
audioPlayer()->play(_current);
|
||||
mixer()->play(_current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +166,7 @@ void Instance::play(const AudioMsgId &audioId) {
|
||||
if (!audioId || !audioId.audio()->song()) {
|
||||
return;
|
||||
}
|
||||
audioPlayer()->play(audioId);
|
||||
mixer()->play(audioId);
|
||||
setCurrent(audioId);
|
||||
if (audioId.audio()->loading()) {
|
||||
documentLoadProgress(audioId.audio());
|
||||
@@ -179,32 +174,30 @@ void Instance::play(const AudioMsgId &audioId) {
|
||||
}
|
||||
|
||||
void Instance::pause() {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing) {
|
||||
if (!(playbackState.state & AudioPlayerStoppedMask)) {
|
||||
if (playbackState.state == AudioPlayerStarting || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerFinishing) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Song);
|
||||
auto state = mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id) {
|
||||
if (!IsStopped(state.state)) {
|
||||
if (state.state == State::Starting || state.state == State::Resuming || state.state == State::Playing || state.state == State::Finishing) {
|
||||
mixer()->pauseresume(AudioMsgId::Type::Song);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::stop() {
|
||||
audioPlayer()->stop(AudioMsgId::Type::Song);
|
||||
mixer()->stop(AudioMsgId::Type::Song);
|
||||
}
|
||||
|
||||
void Instance::playPause() {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing) {
|
||||
if (playbackState.state & AudioPlayerStoppedMask) {
|
||||
audioPlayer()->play(playing);
|
||||
auto state = mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id) {
|
||||
if (IsStopped(state.state)) {
|
||||
mixer()->play(state.id);
|
||||
} else {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Song);
|
||||
mixer()->pauseresume(AudioMsgId::Type::Song);
|
||||
}
|
||||
} else if (_current) {
|
||||
audioPlayer()->play(_current);
|
||||
mixer()->play(_current);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,11 +214,10 @@ void Instance::playPauseCancelClicked() {
|
||||
return;
|
||||
}
|
||||
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
auto stopped = ((playbackState.state & AudioPlayerStoppedMask) || playbackState.state == AudioPlayerFinishing);
|
||||
auto showPause = !stopped && (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
auto audio = playing.audio();
|
||||
auto state = mixer()->currentState(AudioMsgId::Type::Song);
|
||||
auto stopped = (IsStopped(state.state) || state.state == State::Finishing);
|
||||
auto showPause = !stopped && (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
auto audio = state.id.audio();
|
||||
if (audio && audio->loading()) {
|
||||
audio->cancel();
|
||||
} else if (showPause) {
|
||||
@@ -254,23 +246,22 @@ void Instance::documentLoadProgress(DocumentData *document) {
|
||||
|
||||
template <typename CheckCallback>
|
||||
void Instance::emitUpdate(CheckCallback check) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (!playing || !check(playing)) {
|
||||
auto state = mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (!state.id || !check(state.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrent(playing);
|
||||
_updatedNotifier.notify(UpdatedEvent(&playing, &playbackState), true);
|
||||
setCurrent(state.id);
|
||||
_updatedNotifier.notify(state, true);
|
||||
|
||||
if (_isPlaying && playbackState.state == AudioPlayerStoppedAtEnd) {
|
||||
if (_isPlaying && state.state == State::StoppedAtEnd) {
|
||||
if (_repeatEnabled) {
|
||||
audioPlayer()->play(_current);
|
||||
mixer()->play(_current);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
auto isPlaying = !(playbackState.state & AudioPlayerStoppedMask);
|
||||
auto isPlaying = !IsStopped(state.state);
|
||||
if (_isPlaying != isPlaying) {
|
||||
_isPlaying = isPlaying;
|
||||
if (_isPlaying) {
|
||||
|
||||
@@ -23,7 +23,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
namespace Notify {
|
||||
struct PeerUpdate;
|
||||
} // namespace Notify
|
||||
struct AudioPlaybackState;
|
||||
class AudioMsgId;
|
||||
|
||||
namespace Media {
|
||||
@@ -32,20 +31,10 @@ namespace Player {
|
||||
void start();
|
||||
void finish();
|
||||
|
||||
// We use this method instead of checking for instance() != nullptr
|
||||
// because audioPlayer() can be destroyed at any time by an
|
||||
// error in audio playback, so we check it each time.
|
||||
bool exists();
|
||||
|
||||
class Instance;
|
||||
Instance *instance();
|
||||
|
||||
struct UpdatedEvent {
|
||||
UpdatedEvent(const AudioMsgId *audioId, const AudioPlaybackState *playbackState) : audioId(audioId), playbackState(playbackState) {
|
||||
}
|
||||
const AudioMsgId *audioId;
|
||||
const AudioPlaybackState *playbackState;
|
||||
};
|
||||
struct TrackState;
|
||||
|
||||
class Instance : private base::Subscriber {
|
||||
public:
|
||||
@@ -90,7 +79,7 @@ public:
|
||||
base::Observable<bool> &playerWidgetOver() {
|
||||
return _playerWidgetOver;
|
||||
}
|
||||
base::Observable<UpdatedEvent> &updatedNotifier() {
|
||||
base::Observable<TrackState> &updatedNotifier() {
|
||||
return _updatedNotifier;
|
||||
}
|
||||
base::Observable<void> &playlistChangedNotifier() {
|
||||
@@ -136,7 +125,7 @@ private:
|
||||
base::Observable<bool> _usePanelPlayer;
|
||||
base::Observable<bool> _titleButtonOver;
|
||||
base::Observable<bool> _playerWidgetOver;
|
||||
base::Observable<UpdatedEvent> _updatedNotifier;
|
||||
base::Observable<TrackState> _updatedNotifier;
|
||||
base::Observable<void> _playlistChangedNotifier;
|
||||
base::Observable<void> _songChangedNotifier;
|
||||
base::Observable<void> _repeatChangedNotifier;
|
||||
|
||||
@@ -31,9 +31,7 @@ namespace Player {
|
||||
ListWidget::ListWidget(QWidget *parent) : TWidget(parent) {
|
||||
setMouseTracking(true);
|
||||
playlistUpdated();
|
||||
if (exists()) {
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] { playlistUpdated(); });
|
||||
}
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] { playlistUpdated(); });
|
||||
subscribe(Global::RefItemRemoved(), [this](HistoryItem *item) {
|
||||
itemRemoved(item);
|
||||
});
|
||||
@@ -158,17 +156,15 @@ void ListWidget::itemRemoved(HistoryItem *item) {
|
||||
}
|
||||
|
||||
QRect ListWidget::getCurrentTrackGeometry() const {
|
||||
if (exists()) {
|
||||
auto top = marginTop();
|
||||
auto current = instance()->current();
|
||||
auto fullMsgId = current.contextId();
|
||||
for_const (auto layout, _list) {
|
||||
auto layoutHeight = layout->height();
|
||||
if (layout->getItem()->fullId() == fullMsgId) {
|
||||
return QRect(0, top, width(), layoutHeight);
|
||||
}
|
||||
top += layoutHeight;
|
||||
auto top = marginTop();
|
||||
auto current = instance()->current();
|
||||
auto fullMsgId = current.contextId();
|
||||
for_const (auto layout, _list) {
|
||||
auto layoutHeight = layout->height();
|
||||
if (layout->getItem()->fullId() == fullMsgId) {
|
||||
return QRect(0, top, width(), layoutHeight);
|
||||
}
|
||||
top += layoutHeight;
|
||||
}
|
||||
return QRect(0, height(), width(), 0);
|
||||
}
|
||||
@@ -188,8 +184,7 @@ int ListWidget::marginTop() const {
|
||||
void ListWidget::playlistUpdated() {
|
||||
auto newHeight = 0;
|
||||
|
||||
const QList<FullMsgId> emptyPlaylist;
|
||||
auto &playlist = exists() ? instance()->playlist() : emptyPlaylist;
|
||||
auto &playlist = instance()->playlist();
|
||||
auto playlistSize = playlist.size();
|
||||
auto existingSize = _list.size();
|
||||
if (playlistSize > existingSize) {
|
||||
|
||||
@@ -37,7 +37,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
using State = PlayButtonLayout::State;
|
||||
using ButtonState = PlayButtonLayout::State;
|
||||
|
||||
class Widget::PlayButton : public Ui::RippleButton {
|
||||
public:
|
||||
@@ -107,9 +107,7 @@ Widget::Widget(QWidget *parent) : TWidget(parent)
|
||||
handleSeekFinished(value);
|
||||
});
|
||||
_playPause->setClickedCallback([this] {
|
||||
if (exists()) {
|
||||
instance()->playPauseCancelClicked();
|
||||
}
|
||||
instance()->playPauseCancelClicked();
|
||||
});
|
||||
|
||||
updateVolumeToggleIcon();
|
||||
@@ -124,27 +122,22 @@ Widget::Widget(QWidget *parent) : TWidget(parent)
|
||||
instance()->toggleRepeat();
|
||||
});
|
||||
|
||||
if (exists()) {
|
||||
subscribe(instance()->repeatChangedNotifier(), [this] {
|
||||
updateRepeatTrackIcon();
|
||||
});
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] {
|
||||
handlePlaylistUpdate();
|
||||
});
|
||||
subscribe(instance()->updatedNotifier(), [this](const UpdatedEvent &e) {
|
||||
handleSongUpdate(e);
|
||||
});
|
||||
subscribe(instance()->songChangedNotifier(), [this] {
|
||||
handleSongChange();
|
||||
});
|
||||
subscribe(instance()->repeatChangedNotifier(), [this] {
|
||||
updateRepeatTrackIcon();
|
||||
});
|
||||
subscribe(instance()->playlistChangedNotifier(), [this] {
|
||||
handlePlaylistUpdate();
|
||||
});
|
||||
subscribe(instance()->updatedNotifier(), [this](const TrackState &state) {
|
||||
handleSongUpdate(state);
|
||||
});
|
||||
subscribe(instance()->songChangedNotifier(), [this] {
|
||||
handleSongChange();
|
||||
if (auto player = audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = player->currentState(&playing, AudioMsgId::Type::Song);
|
||||
handleSongUpdate(UpdatedEvent(&playing, &playbackState));
|
||||
_playPause->finishTransform();
|
||||
}
|
||||
}
|
||||
});
|
||||
handleSongChange();
|
||||
|
||||
handleSongUpdate(mixer()->currentState(AudioMsgId::Type::Song));
|
||||
_playPause->finishTransform();
|
||||
}
|
||||
|
||||
void Widget::updateVolumeToggleIcon() {
|
||||
@@ -201,9 +194,8 @@ void Widget::handleSeekProgress(float64 progress) {
|
||||
if (_seekPositionMs != positionMs) {
|
||||
_seekPositionMs = positionMs;
|
||||
updateTimeLabel();
|
||||
if (exists()) {
|
||||
instance()->startSeeking();
|
||||
}
|
||||
|
||||
instance()->startSeeking();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,15 +205,13 @@ void Widget::handleSeekFinished(float64 progress) {
|
||||
auto positionMs = snap(static_cast<TimeMs>(progress * _lastDurationMs), 0LL, _lastDurationMs);
|
||||
_seekPositionMs = -1;
|
||||
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing && playbackState.duration) {
|
||||
audioPlayer()->seek(qRound(progress * playbackState.duration));
|
||||
auto type = AudioMsgId::Type::Song;
|
||||
auto state = mixer()->currentState(type);
|
||||
if (state.id && state.duration) {
|
||||
mixer()->seek(type, qRound(progress * state.duration));
|
||||
}
|
||||
|
||||
if (exists()) {
|
||||
instance()->stopSeeking();
|
||||
}
|
||||
instance()->stopSeeking();
|
||||
}
|
||||
|
||||
void Widget::resizeEvent(QResizeEvent *e) {
|
||||
@@ -260,9 +250,7 @@ void Widget::updateOverLabelsState(QPoint pos) {
|
||||
}
|
||||
|
||||
void Widget::updateOverLabelsState(bool over) {
|
||||
if (exists()) {
|
||||
instance()->playerWidgetOver().notify(over, true);
|
||||
}
|
||||
instance()->playerWidgetOver().notify(over, true);
|
||||
}
|
||||
|
||||
void Widget::updatePlayPrevNextPositions() {
|
||||
@@ -311,52 +299,50 @@ void Widget::updateRepeatTrackIcon() {
|
||||
_repeatTrack->setRippleColorOverride(repeating ? nullptr : &st::mediaPlayerRepeatDisabledRippleBg);
|
||||
}
|
||||
|
||||
void Widget::handleSongUpdate(const UpdatedEvent &e) {
|
||||
auto &audioId = *e.audioId;
|
||||
auto &playbackState = *e.playbackState;
|
||||
if (!audioId || !audioId.audio()->song()) {
|
||||
void Widget::handleSongUpdate(const TrackState &state) {
|
||||
if (!state.id || !state.id.audio()->song()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioId.audio()->loading()) {
|
||||
_playback->updateLoadingState(audioId.audio()->progress());
|
||||
if (state.id.audio()->loading()) {
|
||||
_playback->updateLoadingState(state.id.audio()->progress());
|
||||
} else {
|
||||
_playback->updateState(*e.playbackState);
|
||||
_playback->updateState(state);
|
||||
}
|
||||
|
||||
auto stopped = ((playbackState.state & AudioPlayerStoppedMask) || playbackState.state == AudioPlayerFinishing);
|
||||
auto showPause = !stopped && (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
if (exists() && instance()->isSeeking()) {
|
||||
auto stopped = (IsStopped(state.state) || state.state == State::Finishing);
|
||||
auto showPause = !stopped && (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
if (instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
auto state = [audio = audioId.audio(), showPause] {
|
||||
auto buttonState = [audio = state.id.audio(), showPause] {
|
||||
if (audio->loading()) {
|
||||
return State::Cancel;
|
||||
return ButtonState::Cancel;
|
||||
} else if (showPause) {
|
||||
return State::Pause;
|
||||
return ButtonState::Pause;
|
||||
}
|
||||
return State::Play;
|
||||
return ButtonState::Play;
|
||||
};
|
||||
_playPause->setState(state());
|
||||
_playPause->setState(buttonState());
|
||||
|
||||
updateTimeText(audioId, playbackState);
|
||||
updateTimeText(state);
|
||||
}
|
||||
|
||||
void Widget::updateTimeText(const AudioMsgId &audioId, const AudioPlaybackState &playbackState) {
|
||||
void Widget::updateTimeText(const TrackState &state) {
|
||||
QString time;
|
||||
qint64 position = 0, duration = 0, display = 0;
|
||||
auto frequency = (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
if (!(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
display = position = playbackState.position;
|
||||
duration = playbackState.duration;
|
||||
auto frequency = state.frequency;
|
||||
if (!IsStopped(state.state) && state.state != State::Finishing) {
|
||||
display = position = state.position;
|
||||
duration = state.duration;
|
||||
} else {
|
||||
display = playbackState.duration ? playbackState.duration : (audioId.audio()->song()->duration * frequency);
|
||||
display = state.duration ? state.duration : (state.id.audio()->song()->duration * frequency);
|
||||
}
|
||||
|
||||
_lastDurationMs = (playbackState.duration * 1000LL) / frequency;
|
||||
_lastDurationMs = (state.duration * 1000LL) / frequency;
|
||||
|
||||
if (audioId.audio()->loading()) {
|
||||
_time = QString::number(qRound(audioId.audio()->progress() * 100)) + '%';
|
||||
if (state.id.audio()->loading()) {
|
||||
_time = QString::number(qRound(state.id.audio()->progress() * 100)) + '%';
|
||||
_playback->setDisabled(true);
|
||||
} else {
|
||||
display = display / frequency;
|
||||
@@ -425,16 +411,12 @@ void Widget::createPrevNextButtons() {
|
||||
_previousTrack.create(this, st::mediaPlayerPreviousButton);
|
||||
_previousTrack->show();
|
||||
_previousTrack->setClickedCallback([this]() {
|
||||
if (exists()) {
|
||||
instance()->previous();
|
||||
}
|
||||
instance()->previous();
|
||||
});
|
||||
_nextTrack.create(this, st::mediaPlayerNextButton);
|
||||
_nextTrack->show();
|
||||
_nextTrack->setClickedCallback([this]() {
|
||||
if (exists()) {
|
||||
instance()->next();
|
||||
}
|
||||
instance()->next();
|
||||
});
|
||||
updatePlayPrevNextPositions();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#pragma once
|
||||
|
||||
class AudioMsgId;
|
||||
struct AudioPlaybackState;
|
||||
|
||||
namespace Ui {
|
||||
class FlatLabel;
|
||||
@@ -39,7 +38,7 @@ namespace Player {
|
||||
|
||||
class PlayButton;
|
||||
class VolumeWidget;
|
||||
struct UpdatedEvent;
|
||||
struct TrackState;
|
||||
|
||||
class Widget : public TWidget, private base::Subscriber {
|
||||
public:
|
||||
@@ -81,11 +80,11 @@ private:
|
||||
|
||||
void updateVolumeToggleIcon();
|
||||
|
||||
void handleSongUpdate(const UpdatedEvent &e);
|
||||
void handleSongUpdate(const TrackState &state);
|
||||
void handleSongChange();
|
||||
void handlePlaylistUpdate();
|
||||
|
||||
void updateTimeText(const AudioMsgId &audioId, const AudioPlaybackState &playbackState);
|
||||
void updateTimeText(const TrackState &state);
|
||||
void updateTimeLabel();
|
||||
|
||||
TimeMs _seekPositionMs = -1;
|
||||
|
||||
@@ -105,14 +105,14 @@ void Controller::fadeUpdated(float64 opacity) {
|
||||
_playback->setFadeOpacity(opacity);
|
||||
}
|
||||
|
||||
void Controller::updatePlayback(const AudioPlaybackState &playbackState) {
|
||||
updatePlayPauseResumeState(playbackState);
|
||||
_playback->updateState(playbackState);
|
||||
updateTimeTexts(playbackState);
|
||||
void Controller::updatePlayback(const Player::TrackState &state) {
|
||||
updatePlayPauseResumeState(state);
|
||||
_playback->updateState(state);
|
||||
updateTimeTexts(state);
|
||||
}
|
||||
|
||||
void Controller::updatePlayPauseResumeState(const AudioPlaybackState &playbackState) {
|
||||
bool showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || _seekPositionMs >= 0);
|
||||
void Controller::updatePlayPauseResumeState(const Player::TrackState &state) {
|
||||
auto showPause = (state.state == Player::State::Playing || state.state == Player::State::Resuming || _seekPositionMs >= 0);
|
||||
if (showPause != _showPause) {
|
||||
disconnect(_playPauseResume, SIGNAL(clicked()), this, _showPause ? SIGNAL(pausePressed()) : SIGNAL(playPressed()));
|
||||
_showPause = showPause;
|
||||
@@ -122,21 +122,21 @@ void Controller::updatePlayPauseResumeState(const AudioPlaybackState &playbackSt
|
||||
}
|
||||
}
|
||||
|
||||
void Controller::updateTimeTexts(const AudioPlaybackState &playbackState) {
|
||||
qint64 position = 0, duration = playbackState.duration;
|
||||
void Controller::updateTimeTexts(const Player::TrackState &state) {
|
||||
qint64 position = 0, duration = state.duration;
|
||||
|
||||
if (!(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
position = playbackState.position;
|
||||
} else if (playbackState.state == AudioPlayerStoppedAtEnd) {
|
||||
position = playbackState.duration;
|
||||
if (!Player::IsStopped(state.state) && state.state != Player::State::Finishing) {
|
||||
position = state.position;
|
||||
} else if (state.state == Player::State::StoppedAtEnd) {
|
||||
position = state.duration;
|
||||
} else {
|
||||
position = 0;
|
||||
}
|
||||
auto playFrequency = (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
auto playFrequency = state.frequency;
|
||||
auto playAlready = position / playFrequency;
|
||||
auto playLeft = (playbackState.duration / playFrequency) - playAlready;
|
||||
auto playLeft = (state.duration / playFrequency) - playAlready;
|
||||
|
||||
_lastDurationMs = (playbackState.duration * 1000LL) / playFrequency;
|
||||
_lastDurationMs = (state.duration * 1000LL) / playFrequency;
|
||||
|
||||
_timeAlready = formatDurationText(playAlready);
|
||||
auto minus = QChar(8722);
|
||||
|
||||
@@ -26,9 +26,11 @@ class FadeAnimation;
|
||||
class IconButton;
|
||||
} // namespace Ui
|
||||
|
||||
struct AudioPlaybackState;
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
struct TrackState;
|
||||
} // namespace Player
|
||||
|
||||
namespace Clip {
|
||||
|
||||
class Playback;
|
||||
@@ -43,7 +45,7 @@ public:
|
||||
void showAnimated();
|
||||
void hideAnimated();
|
||||
|
||||
void updatePlayback(const AudioPlaybackState &playbackState);
|
||||
void updatePlayback(const Player::TrackState &state);
|
||||
void setInFullScreen(bool inFullScreen);
|
||||
|
||||
void grabStart() override;
|
||||
@@ -74,8 +76,8 @@ private:
|
||||
void fadeFinished();
|
||||
void fadeUpdated(float64 opacity);
|
||||
|
||||
void updatePlayPauseResumeState(const AudioPlaybackState &playbackState);
|
||||
void updateTimeTexts(const AudioPlaybackState &playbackState);
|
||||
void updatePlayPauseResumeState(const Player::TrackState &state);
|
||||
void updateTimeTexts(const Player::TrackState &state);
|
||||
void refreshTimeTexts();
|
||||
|
||||
bool _showPause = false;
|
||||
|
||||
@@ -30,17 +30,17 @@ namespace Clip {
|
||||
Playback::Playback(Ui::ContinuousSlider *slider) : _slider(slider) {
|
||||
}
|
||||
|
||||
void Playback::updateState(const AudioPlaybackState &playbackState) {
|
||||
qint64 position = 0, duration = playbackState.duration;
|
||||
void Playback::updateState(const Player::TrackState &state) {
|
||||
qint64 position = 0, duration = state.duration;
|
||||
|
||||
auto wasDisabled = _slider->isDisabled();
|
||||
if (wasDisabled) setDisabled(false);
|
||||
|
||||
_playing = !(playbackState.state & AudioPlayerStoppedMask);
|
||||
if (_playing || playbackState.state == AudioPlayerStopped) {
|
||||
position = playbackState.position;
|
||||
} else if (playbackState.state == AudioPlayerStoppedAtEnd) {
|
||||
position = playbackState.duration;
|
||||
_playing = !Player::IsStopped(state.state);
|
||||
if (_playing || state.state == Player::State::Stopped) {
|
||||
position = state.position;
|
||||
} else if (state.state == Player::State::StoppedAtEnd) {
|
||||
position = state.duration;
|
||||
} else {
|
||||
position = 0;
|
||||
}
|
||||
|
||||
@@ -22,16 +22,18 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
|
||||
#include "ui/widgets/continuous_sliders.h"
|
||||
|
||||
struct AudioPlaybackState;
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
struct TrackState;
|
||||
} // namespace Player
|
||||
|
||||
namespace Clip {
|
||||
|
||||
class Playback {
|
||||
public:
|
||||
Playback(Ui::ContinuousSlider *slider);
|
||||
|
||||
void updateState(const AudioPlaybackState &playbackState);
|
||||
void updateState(const Player::TrackState &state);
|
||||
void updateLoadingState(float64 progress);
|
||||
|
||||
void setFadeOpacity(float64 opacity) {
|
||||
|
||||
@@ -228,9 +228,7 @@ void MediaView::stopGif() {
|
||||
_videoPaused = _videoStopped = _videoIsSilent = false;
|
||||
_fullScreenVideo = false;
|
||||
_clipController.destroy();
|
||||
if (audioPlayer()) {
|
||||
disconnect(audioPlayer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
|
||||
}
|
||||
disconnect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
|
||||
}
|
||||
|
||||
void MediaView::documentUpdated(DocumentData *doc) {
|
||||
@@ -1487,9 +1485,7 @@ void MediaView::createClipController() {
|
||||
connect(_clipController, SIGNAL(toFullScreenPressed()), this, SLOT(onVideoToggleFullScreen()));
|
||||
connect(_clipController, SIGNAL(fromFullScreenPressed()), this, SLOT(onVideoToggleFullScreen()));
|
||||
|
||||
if (audioPlayer()) {
|
||||
connect(audioPlayer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
|
||||
}
|
||||
connect(Media::Player::mixer(), SIGNAL(updated(const AudioMsgId&)), this, SLOT(onVideoPlayProgress(const AudioMsgId&)));
|
||||
}
|
||||
|
||||
void MediaView::setClipControllerGeometry() {
|
||||
@@ -1540,8 +1536,8 @@ void MediaView::restartVideoAtSeekPosition(TimeMs positionMs) {
|
||||
_videoPaused = _videoIsSilent = _videoStopped = false;
|
||||
_videoPositionMs = positionMs;
|
||||
|
||||
AudioPlaybackState state;
|
||||
state.state = AudioPlayerPlaying;
|
||||
Media::Player::TrackState state;
|
||||
state.state = Media::Player::State::Playing;
|
||||
state.position = _videoPositionMs;
|
||||
state.duration = _videoDurationMs;
|
||||
state.frequency = _videoFrequencyMs;
|
||||
@@ -1585,16 +1581,15 @@ void MediaView::onVideoPlayProgress(const AudioMsgId &audioId) {
|
||||
return;
|
||||
}
|
||||
|
||||
t_assert(audioPlayer() != nullptr);
|
||||
auto state = audioPlayer()->currentVideoState(_gif->playId());
|
||||
auto state = Media::Player::mixer()->currentVideoState(_gif->playId());
|
||||
if (state.duration) {
|
||||
updateVideoPlaybackState(state);
|
||||
}
|
||||
}
|
||||
|
||||
void MediaView::updateVideoPlaybackState(const AudioPlaybackState &state) {
|
||||
void MediaView::updateVideoPlaybackState(const Media::Player::TrackState &state) {
|
||||
if (state.frequency) {
|
||||
if (state.state & AudioPlayerStoppedMask) {
|
||||
if (Media::Player::IsStopped(state.state)) {
|
||||
_videoStopped = true;
|
||||
}
|
||||
_clipController->updatePlayback(state);
|
||||
@@ -1605,13 +1600,13 @@ void MediaView::updateVideoPlaybackState(const AudioPlaybackState &state) {
|
||||
}
|
||||
|
||||
void MediaView::updateSilentVideoPlaybackState() {
|
||||
AudioPlaybackState state;
|
||||
Media::Player::TrackState state;
|
||||
if (_videoPaused) {
|
||||
state.state = AudioPlayerPaused;
|
||||
state.state = Media::Player::State::Paused;
|
||||
} else if (_videoPositionMs == _videoDurationMs) {
|
||||
state.state = AudioPlayerStoppedAtEnd;
|
||||
state.state = Media::Player::State::StoppedAtEnd;
|
||||
} else {
|
||||
state.state = AudioPlayerPlaying;
|
||||
state.state = Media::Player::State::Playing;
|
||||
}
|
||||
state.position = _videoPositionMs;
|
||||
state.duration = _videoDurationMs;
|
||||
|
||||
@@ -24,6 +24,9 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "ui/effects/radial_animation.h"
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
struct TrackState;
|
||||
} // namespace Player
|
||||
namespace Clip {
|
||||
class Controller;
|
||||
} // namespace Clip
|
||||
@@ -45,8 +48,6 @@ namespace Notify {
|
||||
struct PeerUpdate;
|
||||
} // namespace Notify
|
||||
|
||||
struct AudioPlaybackState;
|
||||
|
||||
class MediaView : public TWidget, private base::Subscriber, public RPCSender, public ClickHandlerHost {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -170,7 +171,7 @@ private:
|
||||
void updateCursor();
|
||||
void setZoomLevel(int newZoom);
|
||||
|
||||
void updateVideoPlaybackState(const AudioPlaybackState &state);
|
||||
void updateVideoPlaybackState(const Media::Player::TrackState &state);
|
||||
void updateSilentVideoPlaybackState();
|
||||
void restartVideoAtSeekPosition(TimeMs positionMs);
|
||||
|
||||
|
||||
@@ -649,14 +649,12 @@ bool Voice::updateStatusText() {
|
||||
statusSize = FileStatusSizeFailed;
|
||||
} else if (_data->loaded()) {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
if (audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Voice);
|
||||
if (playing == AudioMsgId(_data, _parent->fullId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
}
|
||||
using State = Media::Player::State;
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
|
||||
if (state.id == AudioMsgId(_data, _parent->fullId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
}
|
||||
} else {
|
||||
statusSize = FileStatusSizeReady;
|
||||
@@ -935,17 +933,15 @@ bool Document::updateStatusText() {
|
||||
} else if (_data->loaded()) {
|
||||
if (_data->song()) {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
if (audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing == AudioMsgId(_data, _parent->fullId()) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
statusSize = -1 - (playbackState.position / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency));
|
||||
realDuration = playbackState.duration / (playbackState.frequency ? playbackState.frequency : AudioVoiceMsgFrequency);
|
||||
showPause = (playbackState.state == AudioPlayerPlaying || playbackState.state == AudioPlayerResuming || playbackState.state == AudioPlayerStarting);
|
||||
}
|
||||
if (!showPause && (playing == AudioMsgId(_data, _parent->fullId())) && Media::Player::exists() && Media::Player::instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
using State = Media::Player::State;
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id == AudioMsgId(_data, _parent->fullId()) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
statusSize = -1 - (state.position / state.frequency);
|
||||
realDuration = (state.duration / state.frequency);
|
||||
showPause = (state.state == State::Playing || state.state == State::Resuming || state.state == State::Starting);
|
||||
}
|
||||
if (!showPause && (state.id == AudioMsgId(_data, _parent->fullId())) && Media::Player::instance()->isSeeking()) {
|
||||
showPause = true;
|
||||
}
|
||||
} else {
|
||||
statusSize = FileStatusSizeLoaded;
|
||||
|
||||
@@ -1215,7 +1215,7 @@ void OverviewInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) {
|
||||
if (document->loading()) {
|
||||
_menu->addAction(lang(lng_context_cancel_download), this, SLOT(cancelContextDownload()))->setEnabled(true);
|
||||
} else {
|
||||
if (document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
|
||||
if (!document->filepath(DocumentData::FilePathResolveChecked).isEmpty()) {
|
||||
_menu->addAction(lang((cPlatform() == dbipMac || cPlatform() == dbipMacOld) ? lng_context_show_in_finder : lng_context_show_in_folder), this, SLOT(showContextInFolder()))->setEnabled(true);
|
||||
}
|
||||
_menu->addAction(lang(lnkIsVideo ? lng_context_save_video : (lnkIsAudio ? lng_context_save_audio : (lnkIsSong ? lng_context_save_audio_file : lng_context_save_file))), App::LambdaDelayed(st::defaultDropdownMenu.menu.ripple.hideDuration, this, [this, document] {
|
||||
@@ -2091,11 +2091,10 @@ int32 OverviewWidget::lastScrollTop() const {
|
||||
}
|
||||
|
||||
int32 OverviewWidget::countBestScroll() const {
|
||||
if (type() == OverviewMusicFiles && audioPlayer()) {
|
||||
AudioMsgId playing;
|
||||
audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing) {
|
||||
int32 top = _inner->itemTop(playing.contextId());
|
||||
if (type() == OverviewMusicFiles) {
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id) {
|
||||
int32 top = _inner->itemTop(state.id.contextId());
|
||||
if (top >= 0) {
|
||||
return snap(top - int(_scroll->height() - (st::msgPadding.top() + st::mediaThumbSize + st::msgPadding.bottom())) / 2, 0, _scroll->scrollTopMax());
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ bool MainWindow::psHasNativeNotifications() {
|
||||
void MainWindow::LibsLoaded() {
|
||||
QStringList cdesktop = QString(getenv("XDG_CURRENT_DESKTOP")).toLower().split(':');
|
||||
noQtTrayIcon = (cdesktop.contains(qstr("pantheon"))) || (cdesktop.contains(qstr("gnome")));
|
||||
tryAppIndicator = cdesktop.contains(qstr("xfce"));
|
||||
tryAppIndicator = (cdesktop.contains(qstr("xfce")) || cdesktop.contains(qstr("unity")));
|
||||
|
||||
if (noQtTrayIcon) cSetSupportTray(false);
|
||||
|
||||
@@ -595,6 +595,16 @@ MainWindow::~MainWindow() {
|
||||
Libs::g_object_unref(_trayMenu);
|
||||
_trayMenu = nullptr;
|
||||
}
|
||||
if (_trayIndicator) {
|
||||
Libs::g_object_unref(_trayIndicator);
|
||||
_trayIndicator = nullptr;
|
||||
}
|
||||
#ifndef TDESKTOP_DISABLE_UNITY_INTEGRATION
|
||||
if (_psUnityLauncherEntry) {
|
||||
Libs::g_object_unref(_psUnityLauncherEntry);
|
||||
_psUnityLauncherEntry = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace Platform
|
||||
|
||||
37
Telegram/SourceFiles/platform/platform_audio.h
Normal file
37
Telegram/SourceFiles/platform/platform_audio.h
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#if defined Q_OS_MAC || defined Q_OS_LINUX
|
||||
namespace Platform {
|
||||
namespace Audio {
|
||||
|
||||
inline void Init() {
|
||||
}
|
||||
|
||||
inline void DeInit() {
|
||||
}
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Platform
|
||||
#elif defined Q_OS_WINRT || defined Q_OS_WIN // Q_OS_MAC || Q_OS_LINUX
|
||||
#include "platform/win/audio_win.h"
|
||||
#endif // Q_OS_MAC || Q_OS_LINUX || Q_OS_WINRT || Q_OS_WIN
|
||||
157
Telegram/SourceFiles/platform/win/audio_win.cpp
Normal file
157
Telegram/SourceFiles/platform/win/audio_win.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#include "stdafx.h"
|
||||
#include "platform/win/audio_win.h"
|
||||
|
||||
#include "media/media_audio.h"
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
#include <wrl\client.h>
|
||||
using namespace Microsoft::WRL;
|
||||
|
||||
namespace Platform {
|
||||
namespace Audio {
|
||||
namespace {
|
||||
|
||||
// Inspired by Chromium.
|
||||
class DeviceListener : public IMMNotificationClient {
|
||||
public:
|
||||
DeviceListener() = default;
|
||||
DeviceListener(const DeviceListener &other) = delete;
|
||||
DeviceListener &operator=(const DeviceListener &other) = delete;
|
||||
virtual ~DeviceListener() = default;
|
||||
|
||||
private:
|
||||
// IMMNotificationClient implementation.
|
||||
STDMETHOD_(ULONG, AddRef)() override {
|
||||
return 1;
|
||||
}
|
||||
STDMETHOD_(ULONG, Release)() override {
|
||||
return 1;
|
||||
}
|
||||
STDMETHOD(QueryInterface)(REFIID iid, void** object) override;
|
||||
STDMETHOD(OnPropertyValueChanged)(LPCWSTR device_id, const PROPERTYKEY key) override;
|
||||
STDMETHOD(OnDeviceAdded)(LPCWSTR device_id) override {
|
||||
return S_OK;
|
||||
}
|
||||
STDMETHOD(OnDeviceRemoved)(LPCWSTR device_id) override {
|
||||
return S_OK;
|
||||
}
|
||||
STDMETHOD(OnDeviceStateChanged)(LPCWSTR device_id, DWORD new_state) override;
|
||||
STDMETHOD(OnDefaultDeviceChanged)(EDataFlow flow, ERole role, LPCWSTR new_default_device_id) override;
|
||||
|
||||
};
|
||||
|
||||
STDMETHODIMP DeviceListener::QueryInterface(REFIID iid, void** object) {
|
||||
if (iid == IID_IUnknown || iid == __uuidof(IMMNotificationClient)) {
|
||||
*object = static_cast<IMMNotificationClient*>(this);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
*object = NULL;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
STDMETHODIMP DeviceListener::OnPropertyValueChanged(LPCWSTR device_id, const PROPERTYKEY key) {
|
||||
LOG(("Audio Info: OnPropertyValueChanged() scheduling detach from audio device."));
|
||||
Media::Player::DetachFromDeviceByTimer();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP DeviceListener::OnDeviceStateChanged(LPCWSTR device_id, DWORD new_state) {
|
||||
LOG(("Audio Info: OnDeviceStateChanged() scheduling detach from audio device."));
|
||||
Media::Player::DetachFromDeviceByTimer();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
STDMETHODIMP DeviceListener::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR new_default_device_id) {
|
||||
// Only listen for console and communication device changes.
|
||||
if ((role != eConsole && role != eCommunications) || (flow != eRender && flow != eCapture)) {
|
||||
LOG(("Audio Info: skipping OnDefaultDeviceChanged() flow %1, role %2, new_default_device_id: %3").arg(flow).arg(role).arg(new_default_device_id ? '"' + QString::fromWCharArray(new_default_device_id) + '"' : QString("nullptr")));
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
LOG(("Audio Info: OnDefaultDeviceChanged() scheduling detach from audio device, flow %1, role %2, new_default_device_id: %3").arg(flow).arg(role).arg(new_default_device_id ? '"' + QString::fromWCharArray(new_default_device_id) + '"' : QString("nullptr")));
|
||||
Media::Player::DetachFromDeviceByTimer();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
auto WasCoInitialized = false;
|
||||
ComPtr<IMMDeviceEnumerator> Enumerator;
|
||||
|
||||
DeviceListener *Listener = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
void Init() {
|
||||
auto hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&Enumerator));
|
||||
if (FAILED(hr)) {
|
||||
Enumerator.Reset();
|
||||
|
||||
if (hr == CO_E_NOTINITIALIZED) {
|
||||
LOG(("Audio Info: CoCreateInstance fails with CO_E_NOTINITIALIZED"));
|
||||
hr = CoInitialize(nullptr);
|
||||
if (SUCCEEDED(hr)) {
|
||||
WasCoInitialized = true;
|
||||
hr = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&Enumerator));
|
||||
if (FAILED(hr)) {
|
||||
Enumerator.Reset();
|
||||
|
||||
LOG(("Audio Error: could not CoCreateInstance of MMDeviceEnumerator, HRESULT: %1").arg(hr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG(("Audio Error: could not CoCreateInstance of MMDeviceEnumerator, HRESULT: %1").arg(hr));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Listener = new DeviceListener();
|
||||
hr = Enumerator->RegisterEndpointNotificationCallback(Listener);
|
||||
if (FAILED(hr)) {
|
||||
LOG(("Audio Error: RegisterEndpointNotificationCallback failed, HRESULT: %1").arg(hr));
|
||||
delete base::take(Listener);
|
||||
}
|
||||
}
|
||||
|
||||
void DeInit() {
|
||||
if (Enumerator) {
|
||||
if (Listener) {
|
||||
auto hr = Enumerator->UnregisterEndpointNotificationCallback(Listener);
|
||||
if (FAILED(hr)) {
|
||||
LOG(("Audio Error: UnregisterEndpointNotificationCallback failed, HRESULT: %1").arg(hr));
|
||||
}
|
||||
delete base::take(Listener);
|
||||
}
|
||||
Enumerator.Reset();
|
||||
}
|
||||
if (WasCoInitialized) {
|
||||
CoUninitialize();
|
||||
}
|
||||
AUDCLNT_E_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Platform
|
||||
31
Telegram/SourceFiles/platform/win/audio_win.h
Normal file
31
Telegram/SourceFiles/platform/win/audio_win.h
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop version of Telegram messaging app, see https://telegram.org
|
||||
|
||||
Telegram Desktop is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
In addition, as a special exception, the copyright holders give permission
|
||||
to link the code of portions of this program with the OpenSSL library.
|
||||
|
||||
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
||||
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Platform {
|
||||
namespace Audio {
|
||||
|
||||
void Init();
|
||||
void DeInit();
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Platform
|
||||
|
||||
@@ -23,6 +23,7 @@ Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
||||
#include "application.h"
|
||||
#include "localstorage.h"
|
||||
#include "media/player/media_player_instance.h"
|
||||
#include "media/media_audio.h"
|
||||
#include "platform/mac/mac_utilities.h"
|
||||
#include "styles/style_window.h"
|
||||
#include "lang.h"
|
||||
@@ -125,6 +126,9 @@ ApplicationDelegate *_sharedDelegate = nil;
|
||||
|
||||
- (void)receiveWakeNote:(NSNotification*)aNotification {
|
||||
if (App::app()) App::app()->checkLocalTime();
|
||||
|
||||
LOG(("Audio Info: -receiveWakeNote: received, scheduling detach from audio device"));
|
||||
Media::Player::DetachFromDeviceByTimer();
|
||||
}
|
||||
|
||||
- (void)setWatchingMediaKeys:(BOOL)watching {
|
||||
@@ -216,27 +220,21 @@ bool objc_handleMediaKeyEvent(void *ev) {
|
||||
switch (keyCode) {
|
||||
case NX_KEYTYPE_PLAY:
|
||||
if (keyState == 0) { // Play pressed and released
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->playPause();
|
||||
}
|
||||
Media::Player::instance()->playPause();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case NX_KEYTYPE_FAST:
|
||||
if (keyState == 0) { // Next pressed and released
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->next();
|
||||
}
|
||||
Media::Player::instance()->next();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case NX_KEYTYPE_REWIND:
|
||||
if (keyState == 0) { // Previous pressed and released
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->previous();
|
||||
}
|
||||
Media::Player::instance()->previous();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -74,9 +74,6 @@ bool gCompressPastedImage = true;
|
||||
|
||||
QString gTimeFormat = qsl("hh:mm");
|
||||
|
||||
bool gHasAudioPlayer = true;
|
||||
bool gHasAudioCapture = true;
|
||||
|
||||
RecentEmojiPack gRecentEmojis;
|
||||
RecentEmojisPreload gRecentEmojisPreload;
|
||||
EmojiColorVariants gEmojiVariants;
|
||||
|
||||
@@ -120,9 +120,6 @@ DeclareSetting(DBIScale, ConfigScale);
|
||||
DeclareSetting(bool, CompressPastedImage);
|
||||
DeclareSetting(QString, TimeFormat);
|
||||
|
||||
DeclareSetting(bool, HasAudioPlayer);
|
||||
DeclareSetting(bool, HasAudioCapture);
|
||||
|
||||
inline void cChangeTimeFormat(const QString &newFormat) {
|
||||
if (!newFormat.isEmpty()) cSetTimeFormat(newFormat);
|
||||
}
|
||||
|
||||
@@ -79,51 +79,33 @@ bool quit_telegram() {
|
||||
//}
|
||||
|
||||
bool media_play() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->play();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->play();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool media_pause() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->pause();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->pause();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool media_playpause() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->playPause();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->playPause();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool media_stop() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->stop();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->stop();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool media_previous() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->previous();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->previous();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool media_next() {
|
||||
if (Media::Player::exists()) {
|
||||
Media::Player::instance()->next();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
Media::Player::instance()->next();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool search() {
|
||||
|
||||
@@ -1161,9 +1161,9 @@ void DocumentOpenClickHandler::doOpen(DocumentData *data, HistoryItem *context,
|
||||
if (!data->date) return;
|
||||
|
||||
auto msgId = context ? context->fullId() : FullMsgId();
|
||||
bool playVoice = data->voice() && audioPlayer();
|
||||
bool playMusic = data->song() && audioPlayer();
|
||||
bool playVideo = data->isVideo() && audioPlayer();
|
||||
bool playVoice = data->voice();
|
||||
bool playMusic = data->song();
|
||||
bool playVideo = data->isVideo();
|
||||
bool playAnimation = data->isAnimation();
|
||||
auto &location = data->location(true);
|
||||
if (auto applyTheme = data->isTheme()) {
|
||||
@@ -1174,28 +1174,27 @@ void DocumentOpenClickHandler::doOpen(DocumentData *data, HistoryItem *context,
|
||||
}
|
||||
}
|
||||
if (!location.isEmpty() || (!data->data().isEmpty() && (playVoice || playMusic || playVideo || playAnimation))) {
|
||||
using State = Media::Player::State;
|
||||
if (playVoice) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Voice);
|
||||
if (playing == AudioMsgId(data, msgId) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Voice);
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
|
||||
if (state.id == AudioMsgId(data, msgId) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
Media::Player::mixer()->pauseresume(AudioMsgId::Type::Voice);
|
||||
} else {
|
||||
AudioMsgId audio(data, msgId);
|
||||
audioPlayer()->play(audio);
|
||||
audioPlayer()->notify(audio);
|
||||
auto audio = AudioMsgId(data, msgId);
|
||||
Media::Player::mixer()->play(audio);
|
||||
Media::Player::Updated().notify(audio);
|
||||
if (App::main()) {
|
||||
App::main()->mediaMarkRead(data);
|
||||
}
|
||||
}
|
||||
} else if (playMusic) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing == AudioMsgId(data, msgId) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Song);
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id == AudioMsgId(data, msgId) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
Media::Player::mixer()->pauseresume(AudioMsgId::Type::Song);
|
||||
} else {
|
||||
AudioMsgId song(data, msgId);
|
||||
audioPlayer()->play(song);
|
||||
audioPlayer()->notify(song);
|
||||
auto song = AudioMsgId(data, msgId);
|
||||
Media::Player::mixer()->play(song);
|
||||
Media::Player::Updated().notify(song);
|
||||
}
|
||||
} else if (playVideo) {
|
||||
if (!data->data().isEmpty()) {
|
||||
@@ -1470,8 +1469,8 @@ void DocumentData::performActionOnLoad() {
|
||||
auto already = loc.name();
|
||||
auto item = _actionOnLoadMsgId.msg ? App::histItemById(_actionOnLoadMsgId) : nullptr;
|
||||
bool showImage = !isVideo() && (size < App::kImageSizeLimit);
|
||||
bool playVoice = voice() && audioPlayer() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
|
||||
bool playMusic = song() && audioPlayer() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
|
||||
bool playVoice = voice() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
|
||||
bool playMusic = song() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen);
|
||||
bool playAnimation = isAnimation() && (_actionOnLoad == ActionOnLoadPlayInline || _actionOnLoad == ActionOnLoadOpen) && showImage && item && item->getMedia();
|
||||
if (auto applyTheme = isTheme()) {
|
||||
if (!loc.isEmpty() && loc.accessEnable()) {
|
||||
@@ -1480,27 +1479,26 @@ void DocumentData::performActionOnLoad() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
using State = Media::Player::State;
|
||||
if (playVoice) {
|
||||
if (loaded()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Voice);
|
||||
if (playing == AudioMsgId(this, _actionOnLoadMsgId) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Voice);
|
||||
} else if (playbackState.state & AudioPlayerStoppedMask) {
|
||||
audioPlayer()->play(AudioMsgId(this, _actionOnLoadMsgId));
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Voice);
|
||||
if (state.id == AudioMsgId(this, _actionOnLoadMsgId) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
Media::Player::mixer()->pauseresume(AudioMsgId::Type::Voice);
|
||||
} else if (Media::Player::IsStopped(state.state)) {
|
||||
Media::Player::mixer()->play(AudioMsgId(this, _actionOnLoadMsgId));
|
||||
if (App::main()) App::main()->mediaMarkRead(this);
|
||||
}
|
||||
}
|
||||
} else if (playMusic) {
|
||||
if (loaded()) {
|
||||
AudioMsgId playing;
|
||||
auto playbackState = audioPlayer()->currentState(&playing, AudioMsgId::Type::Song);
|
||||
if (playing == AudioMsgId(this, _actionOnLoadMsgId) && !(playbackState.state & AudioPlayerStoppedMask) && playbackState.state != AudioPlayerFinishing) {
|
||||
audioPlayer()->pauseresume(AudioMsgId::Type::Song);
|
||||
} else if (playbackState.state & AudioPlayerStoppedMask) {
|
||||
AudioMsgId song(this, _actionOnLoadMsgId);
|
||||
audioPlayer()->play(song);
|
||||
audioPlayer()->notify(song);
|
||||
auto state = Media::Player::mixer()->currentState(AudioMsgId::Type::Song);
|
||||
if (state.id == AudioMsgId(this, _actionOnLoadMsgId) && !Media::Player::IsStopped(state.state) && state.state != State::Finishing) {
|
||||
Media::Player::mixer()->pauseresume(AudioMsgId::Type::Song);
|
||||
} else if (Media::Player::IsStopped(state.state)) {
|
||||
auto song = AudioMsgId(this, _actionOnLoadMsgId);
|
||||
Media::Player::mixer()->play(song);
|
||||
Media::Player::Updated().notify(song);
|
||||
}
|
||||
}
|
||||
} else if (playAnimation) {
|
||||
|
||||
@@ -30,7 +30,7 @@ windowDefaultHeight: 600px;
|
||||
windowShadow: icon {{ "window_shadow", windowShadowFg }};
|
||||
windowShadowShift: 1px;
|
||||
|
||||
adaptiveChatWideWidth: 860px;
|
||||
adaptiveChatWideWidth: 880px;
|
||||
|
||||
notifyBorder: windowShadowFgFallback;
|
||||
notifyBorderWidth: 1px;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
AppVersion 1000002
|
||||
AppVersion 1000003
|
||||
AppVersionStrMajor 1.0
|
||||
AppVersionStrSmall 1.0.2
|
||||
AppVersionStr 1.0.2
|
||||
AlphaChannel 0
|
||||
AppVersionStrSmall 1.0.3
|
||||
AppVersionStr 1.0.3
|
||||
AlphaChannel 1
|
||||
BetaVersion 0
|
||||
|
||||
@@ -301,6 +301,8 @@
|
||||
'<(src_loc)/media/view/media_clip_volume_controller.h',
|
||||
'<(src_loc)/media/media_audio.cpp',
|
||||
'<(src_loc)/media/media_audio.h',
|
||||
'<(src_loc)/media/media_audio_capture.cpp',
|
||||
'<(src_loc)/media/media_audio_capture.h',
|
||||
'<(src_loc)/media/media_audio_ffmpeg_loader.cpp',
|
||||
'<(src_loc)/media/media_audio_ffmpeg_loader.h',
|
||||
'<(src_loc)/media/media_audio_loader.cpp',
|
||||
@@ -378,6 +380,8 @@
|
||||
'<(src_loc)/platform/mac/notifications_manager_mac.h',
|
||||
'<(src_loc)/platform/mac/window_title_mac.mm',
|
||||
'<(src_loc)/platform/mac/window_title_mac.h',
|
||||
'<(src_loc)/platform/win/audio_win.cpp',
|
||||
'<(src_loc)/platform/win/audio_win.h',
|
||||
'<(src_loc)/platform/win/main_window_win.cpp',
|
||||
'<(src_loc)/platform/win/main_window_win.h',
|
||||
'<(src_loc)/platform/win/notifications_manager_win.cpp',
|
||||
@@ -390,6 +394,7 @@
|
||||
'<(src_loc)/platform/win/windows_dlls.h',
|
||||
'<(src_loc)/platform/win/windows_event_filter.cpp',
|
||||
'<(src_loc)/platform/win/windows_event_filter.h',
|
||||
'<(src_loc)/platform/platform_audio.h',
|
||||
'<(src_loc)/platform/platform_file_dialog.h',
|
||||
'<(src_loc)/platform/platform_main_window.h',
|
||||
'<(src_loc)/platform/platform_notifications_manager.h',
|
||||
@@ -642,12 +647,14 @@
|
||||
'sources!': [
|
||||
'<(src_loc)/pspecific_win.cpp',
|
||||
'<(src_loc)/pspecific_win.h',
|
||||
'<(src_loc)/platform/win/audio_win.cpp',
|
||||
'<(src_loc)/platform/win/audio_win.h',
|
||||
'<(src_loc)/platform/win/main_window_win.cpp',
|
||||
'<(src_loc)/platform/win/main_window_win.h',
|
||||
'<(src_loc)/platform/win/notifications_manager_win.cpp',
|
||||
'<(src_loc)/platform/win/notifications_manager_win.h',
|
||||
'<(src_loc)/platform/win/window_title_win.cpp',
|
||||
'<(src_loc)/platform/win/window_title_win.h',
|
||||
'<(src_loc)/platform/win/window_title_win.cpp',
|
||||
'<(src_loc)/platform/win/window_title_win.h',
|
||||
'<(src_loc)/platform/win/windows_app_user_model_id.cpp',
|
||||
'<(src_loc)/platform/win/windows_app_user_model_id.h',
|
||||
'<(src_loc)/platform/win/windows_dlls.cpp',
|
||||
|
||||
Reference in New Issue
Block a user