Compare commits

...

6 Commits

Author SHA1 Message Date
John Preston
697fdd4294 Alpha 1.0.3: audio device opened only when some sound is played.
Also on Windows Vista and later audio device should switch after
the system default audio output device is changed.
2017-01-27 10:08:59 +03:00
John Preston
4964b8b488 Beta 1000002001: Recreate audio device when no output or device changes. 2017-01-25 08:45:09 +03:00
John Preston
6ae68b337d Audio capture moved to a separate module. 2017-01-25 00:25:21 +03:00
Marco Trevisan
28899a642b main-window-linux: unref tray Indicator and Unity Launcher Entry (#2661)
Signed-off-by: Marco Trevisan <mail@3v1n0.net> (github: 3v1n0)
2017-01-24 13:45:10 +03:00
Marco Trevisan
4518e94c8a main-window-linux: try to use AppIndicator in Unity (#2660)
Signed-off-by: Marco Trevisan <mail@3v1n0.net> (github: 3v1n0)
2017-01-24 13:44:46 +03:00
ZunSThy
59b0b0659a Fix: Corret 'Show in folder' logic (#2894)
Signed-off-by: Zun SThy <zunsthy@gmail.com> (github: zunsthy)
2017-01-24 13:18:48 +03:00
48 changed files with 2684 additions and 2099 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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();
}
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View 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

View 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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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());
}

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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;

View File

@@ -74,9 +74,6 @@ bool gCompressPastedImage = true;
QString gTimeFormat = qsl("hh:mm");
bool gHasAudioPlayer = true;
bool gHasAudioCapture = true;
RecentEmojiPack gRecentEmojis;
RecentEmojisPreload gRecentEmojisPreload;
EmojiColorVariants gEmojiVariants;

View File

@@ -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);
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -30,7 +30,7 @@ windowDefaultHeight: 600px;
windowShadow: icon {{ "window_shadow", windowShadowFg }};
windowShadowShift: 1px;
adaptiveChatWideWidth: 860px;
adaptiveChatWideWidth: 880px;
notifyBorder: windowShadowFgFallback;
notifyBorderWidth: 1px;

View File

@@ -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

View File

@@ -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',