init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s
Close stale issues and PRs / stale (push) Has been cancelled
This commit is contained in:
1630
Telegram/SourceFiles/media/audio/media_audio.cpp
Normal file
1630
Telegram/SourceFiles/media/audio/media_audio.cpp
Normal file
File diff suppressed because it is too large
Load Diff
424
Telegram/SourceFiles/media/audio/media_audio.h
Normal file
424
Telegram/SourceFiles/media/audio/media_audio.h
Normal file
@@ -0,0 +1,424 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ui/effects/animation_value.h"
|
||||
#include "core/file_location.h"
|
||||
#include "data/data_audio_msg_id.h"
|
||||
#include "base/bytes.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
namespace Ui {
|
||||
struct PreparedFileInformation;
|
||||
} // namespace Ui
|
||||
|
||||
namespace Media {
|
||||
struct ExternalSoundData;
|
||||
struct ExternalSoundPart;
|
||||
} // namespace Media
|
||||
|
||||
namespace Media {
|
||||
namespace Streaming {
|
||||
struct TimePoint;
|
||||
} // namespace Streaming
|
||||
} // namespace Media
|
||||
|
||||
namespace Webrtc {
|
||||
struct DeviceResolvedId;
|
||||
} // namespace Webrtc
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
|
||||
class Instance;
|
||||
|
||||
// Thread: Main.
|
||||
void Start(not_null<Instance*> instance);
|
||||
void Finish(not_null<Instance*> instance);
|
||||
|
||||
// Thread: Main. Locks: AudioMutex.
|
||||
bool IsAttachedToDevice();
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
bool AttachToDevice();
|
||||
|
||||
// Thread: Any.
|
||||
void ScheduleDetachFromDeviceSafe();
|
||||
void ScheduleDetachIfNotUsedSafe();
|
||||
void StopDetachIfNotUsedSafe();
|
||||
bool SupportsSpeedControl();
|
||||
|
||||
} // namespace Audio
|
||||
|
||||
namespace Player {
|
||||
|
||||
constexpr auto kDefaultFrequency = 48000; // 48 kHz
|
||||
constexpr auto kTogetherLimit = 4;
|
||||
constexpr auto kWaveformSamplesCount = 100;
|
||||
|
||||
class Fader;
|
||||
class Loaders;
|
||||
|
||||
[[nodiscard]] rpl::producer<AudioMsgId> Updated();
|
||||
|
||||
float64 ComputeVolume(AudioMsgId::Type type);
|
||||
|
||||
enum class State {
|
||||
Stopped = 0x01,
|
||||
StoppedAtEnd = 0x02,
|
||||
StoppedAtError = 0x03,
|
||||
StoppedAtStart = 0x04,
|
||||
|
||||
Starting = 0x08,
|
||||
Playing = 0x10,
|
||||
Stopping = 0x18,
|
||||
Pausing = 0x20,
|
||||
Paused = 0x28,
|
||||
PausedAtEnd = 0x30,
|
||||
Resuming = 0x38,
|
||||
};
|
||||
|
||||
inline bool IsStopped(State state) {
|
||||
return (state == State::Stopped)
|
||||
|| (state == State::StoppedAtEnd)
|
||||
|| (state == State::StoppedAtError)
|
||||
|| (state == State::StoppedAtStart);
|
||||
}
|
||||
|
||||
inline bool IsStoppedOrStopping(State state) {
|
||||
return IsStopped(state) || (state == State::Stopping);
|
||||
}
|
||||
|
||||
inline bool IsStoppedAtEnd(State state) {
|
||||
return (state == State::StoppedAtEnd);
|
||||
}
|
||||
|
||||
inline bool IsPaused(State state) {
|
||||
return (state == State::Paused)
|
||||
|| (state == State::PausedAtEnd);
|
||||
}
|
||||
|
||||
inline bool IsPausedOrPausing(State state) {
|
||||
return IsPaused(state) || (state == State::Pausing);
|
||||
}
|
||||
|
||||
inline bool IsFading(State state) {
|
||||
return (state == State::Starting)
|
||||
|| (state == State::Stopping)
|
||||
|| (state == State::Pausing)
|
||||
|| (state == State::Resuming);
|
||||
}
|
||||
|
||||
inline bool IsActive(State state) {
|
||||
return !IsStopped(state) && !IsPaused(state);
|
||||
}
|
||||
|
||||
inline bool ShowPauseIcon(State state) {
|
||||
return !IsStoppedOrStopping(state)
|
||||
&& !IsPausedOrPausing(state);
|
||||
}
|
||||
|
||||
struct TrackState {
|
||||
AudioMsgId id;
|
||||
State state = State::Stopped;
|
||||
int64 position = 0;
|
||||
int64 receivedTill = 0;
|
||||
int64 length = 0;
|
||||
int frequency = kDefaultFrequency;
|
||||
int fileHeaderSize = 0;
|
||||
bool waitingForData = false;
|
||||
};
|
||||
|
||||
class Mixer final : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Mixer(not_null<Audio::Instance*> instance);
|
||||
|
||||
void play(
|
||||
const AudioMsgId &audio,
|
||||
std::unique_ptr<ExternalSoundData> externalData,
|
||||
crl::time positionMs);
|
||||
void pause(const AudioMsgId &audio, bool fast = false);
|
||||
void resume(const AudioMsgId &audio, bool fast = false);
|
||||
void stop(const AudioMsgId &audio);
|
||||
void stop(const AudioMsgId &audio, State state);
|
||||
|
||||
// External player audio stream interface.
|
||||
void feedFromExternal(ExternalSoundPart &&part);
|
||||
void forceToBufferExternal(const AudioMsgId &audioId);
|
||||
|
||||
// Thread: Main. Locks: AudioMutex.
|
||||
void setSpeedFromExternal(const AudioMsgId &audioId, float64 speed);
|
||||
|
||||
Streaming::TimePoint getExternalSyncTimePoint(
|
||||
const AudioMsgId &audio) const;
|
||||
crl::time getExternalCorrectedTime(
|
||||
const AudioMsgId &id,
|
||||
crl::time frameMs,
|
||||
crl::time systemMs);
|
||||
|
||||
void stopAndClear();
|
||||
|
||||
TrackState currentState(AudioMsgId::Type type);
|
||||
|
||||
// Thread: Main. Must be locked: AudioMutex.
|
||||
void prepareToCloseDevice();
|
||||
|
||||
// Thread: Main. Must be locked: AudioMutex.
|
||||
void reattachIfNeeded();
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
void reattachTracks();
|
||||
|
||||
// Thread: Any.
|
||||
void setSongVolume(float64 volume);
|
||||
float64 getSongVolume() const;
|
||||
void setVideoVolume(float64 volume);
|
||||
float64 getVideoVolume() const;
|
||||
|
||||
void scheduleFaderCallback();
|
||||
|
||||
~Mixer();
|
||||
|
||||
private Q_SLOTS:
|
||||
void onError(const AudioMsgId &audio);
|
||||
void onStopped(const AudioMsgId &audio);
|
||||
|
||||
void onUpdated(const AudioMsgId &audio);
|
||||
|
||||
Q_SIGNALS:
|
||||
void updated(const AudioMsgId &audio);
|
||||
void stoppedOnError(const AudioMsgId &audio);
|
||||
void loaderOnStart(const AudioMsgId &audio, qint64 positionMs);
|
||||
void loaderOnCancel(const AudioMsgId &audio);
|
||||
|
||||
void suppressSong();
|
||||
void unsuppressSong();
|
||||
void suppressAll(qint64 duration);
|
||||
|
||||
private:
|
||||
class Track {
|
||||
public:
|
||||
static constexpr int kBuffersCount = 3;
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
void reattach(AudioMsgId::Type type);
|
||||
|
||||
// Thread: Main. Must be locked: AudioMutex.
|
||||
void detach();
|
||||
void clear();
|
||||
|
||||
void started();
|
||||
|
||||
bool isStreamCreated() const;
|
||||
void ensureStreamCreated(AudioMsgId::Type type);
|
||||
|
||||
int getNotQueuedBufferIndex();
|
||||
|
||||
// Thread: Main. Must be locked: AudioMutex.
|
||||
void setExternalData(std::unique_ptr<ExternalSoundData> data);
|
||||
|
||||
void updateStatePosition();
|
||||
void updateWithSpeedPosition();
|
||||
|
||||
[[nodiscard]] static int64 SpeedIndependentPosition(
|
||||
int64 position,
|
||||
float64 speed);
|
||||
[[nodiscard]] static int64 SpeedDependentPosition(
|
||||
int64 position,
|
||||
float64 speed);
|
||||
|
||||
~Track();
|
||||
|
||||
TrackState state;
|
||||
|
||||
Core::FileLocation file;
|
||||
QByteArray data;
|
||||
|
||||
int format = 0;
|
||||
bool loading = false;
|
||||
bool loaded = false;
|
||||
bool waitingForBuffer = false;
|
||||
|
||||
// Speed dependent values.
|
||||
float64 speed = 1.;
|
||||
float64 nextSpeed = 1.;
|
||||
struct WithSpeed {
|
||||
int64 fineTunedPosition = 0;
|
||||
int64 position = 0;
|
||||
int64 length = 0;
|
||||
int64 bufferedPosition = 0;
|
||||
int64 bufferedLength = 0;
|
||||
int64 fadeStartPosition = 0;
|
||||
int samples[kBuffersCount] = { 0 };
|
||||
QByteArray buffered[kBuffersCount];
|
||||
};
|
||||
WithSpeed withSpeed;
|
||||
|
||||
struct Stream {
|
||||
uint32 source = 0;
|
||||
uint32 buffers[kBuffersCount] = { 0 };
|
||||
};
|
||||
Stream stream;
|
||||
|
||||
std::unique_ptr<ExternalSoundData> externalData;
|
||||
|
||||
crl::time lastUpdateWhen = 0;
|
||||
crl::time lastUpdatePosition = 0;
|
||||
|
||||
private:
|
||||
void createStream(AudioMsgId::Type type);
|
||||
void destroyStream();
|
||||
void resetStream();
|
||||
|
||||
};
|
||||
|
||||
bool fadedStop(AudioMsgId::Type type, bool *fadedStart = 0);
|
||||
void resetFadeStartPosition(AudioMsgId::Type type, int positionInBuffered = -1);
|
||||
bool checkCurrentALError(AudioMsgId::Type type);
|
||||
|
||||
void externalSoundProgress(const AudioMsgId &audio);
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
void setStoppedState(Track *current, State state = State::Stopped);
|
||||
|
||||
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;
|
||||
|
||||
const not_null<Audio::Instance*> _instance;
|
||||
|
||||
int _audioCurrent = 0;
|
||||
Track _audioTracks[kTogetherLimit];
|
||||
|
||||
int _songCurrent = 0;
|
||||
Track _songTracks[kTogetherLimit];
|
||||
|
||||
Track _videoTrack;
|
||||
|
||||
QAtomicInt _volumeVideo;
|
||||
QAtomicInt _volumeSong;
|
||||
|
||||
friend class Fader;
|
||||
friend class Loaders;
|
||||
|
||||
QThread _faderThread, _loaderThread;
|
||||
Fader *_fader;
|
||||
Loaders *_loader;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
Mixer *mixer();
|
||||
|
||||
class Fader : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Fader(QThread *thread);
|
||||
|
||||
void songVolumeChanged();
|
||||
void videoVolumeChanged();
|
||||
|
||||
Q_SIGNALS:
|
||||
void error(const AudioMsgId &audio);
|
||||
void playPositionUpdated(const AudioMsgId &audio);
|
||||
void audioStopped(const AudioMsgId &audio);
|
||||
void needToPreload(const AudioMsgId &audio);
|
||||
|
||||
public Q_SLOTS:
|
||||
void onInit();
|
||||
void onTimer();
|
||||
|
||||
void onSuppressSong();
|
||||
void onUnsuppressSong();
|
||||
void onSuppressAll(qint64 duration);
|
||||
|
||||
private:
|
||||
enum {
|
||||
EmitError = 0x01,
|
||||
EmitStopped = 0x02,
|
||||
EmitPositionUpdated = 0x04,
|
||||
EmitNeedToPreload = 0x08,
|
||||
};
|
||||
int32 updateOnePlayback(Mixer::Track *track, bool &hasPlaying, bool &hasFading, float64 volumeMultiplier, bool volumeChanged);
|
||||
void setStoppedState(Mixer::Track *track, State state = State::Stopped);
|
||||
|
||||
QTimer _timer;
|
||||
|
||||
bool _volumeChangedSong = false;
|
||||
bool _volumeChangedVideo = false;
|
||||
|
||||
bool _suppressAll = false;
|
||||
bool _suppressAllAnim = false;
|
||||
bool _suppressSong = false;
|
||||
bool _suppressSongAnim = false;
|
||||
anim::value _suppressVolumeAll;
|
||||
anim::value _suppressVolumeSong;
|
||||
crl::time _suppressAllStart = 0;
|
||||
crl::time _suppressAllEnd = 0;
|
||||
crl::time _suppressSongStart = 0;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Ui::PreparedFileInformation PrepareForSending(
|
||||
const QString &fname,
|
||||
const QByteArray &data);
|
||||
|
||||
namespace internal {
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
bool CheckAudioDeviceConnected();
|
||||
|
||||
// Thread: Main. Locks: AudioMutex.
|
||||
void DetachFromDevice(not_null<Audio::Instance*> instance);
|
||||
bool DetachIfDeviceChanged(
|
||||
not_null<Audio::Instance*> instance,
|
||||
const Webrtc::DeviceResolvedId &nowDeviceId);
|
||||
|
||||
// Thread: Any.
|
||||
QMutex *audioPlayerMutex();
|
||||
|
||||
// Thread: Any.
|
||||
bool audioCheckError();
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
|
||||
VoiceWaveform audioCountWaveform(const Core::FileLocation &file, const QByteArray &data);
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
|
||||
TG_FORCE_INLINE uint16 ReadOneSample(uchar data) {
|
||||
return qAbs((static_cast<int16>(data) - 0x80) * 0x100);
|
||||
}
|
||||
|
||||
TG_FORCE_INLINE uint16 ReadOneSample(int16 data) {
|
||||
return qAbs(data);
|
||||
}
|
||||
|
||||
template <typename SampleType, typename Callback>
|
||||
void IterateSamples(bytes::const_span bytes, Callback &&callback) {
|
||||
auto samplesPointer = reinterpret_cast<const SampleType*>(bytes.data());
|
||||
auto samplesCount = bytes.size() / sizeof(SampleType);
|
||||
auto samplesData = gsl::make_span(samplesPointer, samplesCount);
|
||||
for (auto sampleData : samplesData) {
|
||||
callback(ReadOneSample(sampleData));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Media
|
||||
859
Telegram/SourceFiles/media/audio/media_audio_capture.cpp
Normal file
859
Telegram/SourceFiles/media/audio/media_audio_capture.cpp
Normal file
@@ -0,0 +1,859 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_capture.h"
|
||||
|
||||
#include "media/audio/media_audio_capture_common.h"
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
#include "media/audio/media_audio_track.h"
|
||||
#include "ffmpeg/ffmpeg_utility.h"
|
||||
#include "base/timer.h"
|
||||
|
||||
#include <al.h>
|
||||
#include <alc.h>
|
||||
|
||||
#include <numeric>
|
||||
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
namespace {
|
||||
|
||||
constexpr auto kCaptureFrequency = Player::kDefaultFrequency;
|
||||
constexpr auto kCaptureSkipDuration = crl::time(400);
|
||||
constexpr auto kCaptureFadeInDuration = crl::time(300);
|
||||
constexpr auto kCaptureBufferSlice = 256 * 1024;
|
||||
constexpr auto kCaptureUpdateDelta = crl::time(100);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
[[nodiscard]] VoiceWaveform CollectWaveform(
|
||||
const QVector<uchar> &waveformVector) {
|
||||
if (waveformVector.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
auto waveform = VoiceWaveform();
|
||||
auto count = int64(waveformVector.size());
|
||||
auto sum = int64(0);
|
||||
if (count >= Player::kWaveformSamplesCount) {
|
||||
auto peaks = QVector<uint16>();
|
||||
peaks.reserve(Player::kWaveformSamplesCount);
|
||||
|
||||
auto peak = uint16(0);
|
||||
for (auto i = int32(0); i < count; ++i) {
|
||||
auto sample = uint16(waveformVector.at(i)) * 256;
|
||||
if (peak < sample) {
|
||||
peak = sample;
|
||||
}
|
||||
sum += Player::kWaveformSamplesCount;
|
||||
if (sum >= count) {
|
||||
sum -= count;
|
||||
peaks.push_back(peak);
|
||||
peak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto sum = std::accumulate(peaks.cbegin(), peaks.cend(), 0LL);
|
||||
peak = qMax(int32(sum * 1.8 / peaks.size()), 2500);
|
||||
|
||||
waveform.resize(peaks.size());
|
||||
for (int32 i = 0, l = peaks.size(); i != l; ++i) {
|
||||
waveform[i] = char(qMin(
|
||||
31U,
|
||||
uint32(qMin(peaks.at(i), peak)) * 31 / peak));
|
||||
}
|
||||
}
|
||||
return waveform;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class Instance::Inner final : public QObject {
|
||||
public:
|
||||
Inner(QThread *thread);
|
||||
~Inner();
|
||||
|
||||
void start(
|
||||
Webrtc::DeviceResolvedId id,
|
||||
Fn<void(Update)> updated,
|
||||
Fn<void()> error,
|
||||
Fn<void(Chunk)> externalProcessing);
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
void pause(bool value, Fn<void(Result&&)> callback);
|
||||
|
||||
private:
|
||||
void process();
|
||||
|
||||
bool initializeFFmpeg();
|
||||
[[nodiscard]] bool processFrame(int32 offset, int32 framesize);
|
||||
void fail();
|
||||
|
||||
[[nodiscard]] bool writeFrame(AVFrame *frame);
|
||||
|
||||
// Writes the packets till EAGAIN is got from av_receive_packet()
|
||||
// Returns number of packets written or -1 on error
|
||||
[[nodiscard]] int writePackets();
|
||||
|
||||
Fn<void(Chunk)> _externalProcessing;
|
||||
Fn<void(Update)> _updated;
|
||||
Fn<void()> _error;
|
||||
|
||||
struct Private;
|
||||
const std::unique_ptr<Private> d;
|
||||
base::Timer _timer;
|
||||
QByteArray _captured;
|
||||
|
||||
bool _paused = false;
|
||||
|
||||
};
|
||||
|
||||
void Start() {
|
||||
Assert(CaptureInstance == nullptr);
|
||||
CaptureInstance = new Instance();
|
||||
instance()->check();
|
||||
}
|
||||
|
||||
void Finish() {
|
||||
delete base::take(CaptureInstance);
|
||||
}
|
||||
|
||||
Instance::Instance() : _inner(std::make_unique<Inner>(&_thread)) {
|
||||
CaptureInstance = this;
|
||||
_thread.start();
|
||||
}
|
||||
|
||||
void Instance::start(Fn<void(Chunk)> externalProcessing) {
|
||||
_updates.fire_done();
|
||||
const auto id = Audio::Current().captureDeviceId();
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
_inner->start(id, [=](Update update) {
|
||||
crl::on_main(this, [=] {
|
||||
_updates.fire_copy(update);
|
||||
});
|
||||
}, [=] {
|
||||
crl::on_main(this, [=] {
|
||||
_updates.fire_error(Error::Other);
|
||||
});
|
||||
}, externalProcessing);
|
||||
crl::on_main(this, [=] {
|
||||
_started = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::stop(Fn<void(Result&&)> callback) {
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
if (!callback) {
|
||||
_inner->stop();
|
||||
crl::on_main(this, [=] { _started = false; });
|
||||
return;
|
||||
}
|
||||
_inner->stop([=](Result &&result) {
|
||||
crl::on_main([=, result = std::move(result)]() mutable {
|
||||
callback(std::move(result));
|
||||
_started = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::pause(bool value, Fn<void(Result&&)> callback) {
|
||||
InvokeQueued(_inner.get(), [=] {
|
||||
auto done = callback
|
||||
? [=](Result &&result) {
|
||||
crl::on_main([=, result = std::move(result)]() mutable {
|
||||
callback(std::move(result));
|
||||
});
|
||||
}
|
||||
: std::move(callback);
|
||||
_inner->pause(value, std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
void Instance::check() {
|
||||
_available = false;
|
||||
if (auto device = alcGetString(0, ALC_CAPTURE_DEFAULT_DEVICE_SPECIFIER)) {
|
||||
if (!QString::fromUtf8(device).isEmpty()) {
|
||||
_available = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
LOG(("Audio Error: No capture device found!"));
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
// Send _inner to it's thread for destruction.
|
||||
if (const auto context = _inner.get()) {
|
||||
InvokeQueued(context, [copy = base::take(_inner)]{});
|
||||
}
|
||||
|
||||
// And wait for it to finish.
|
||||
_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;
|
||||
const AVCodec *codec = nullptr;
|
||||
AVCodecContext *codecContext = nullptr;
|
||||
int channels = 0;
|
||||
bool opened = false;
|
||||
bool processing = 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 ReadData(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 AVERROR_EOF;
|
||||
}
|
||||
|
||||
memcpy(buf, l->data.constData() + l->dataPos, nbytes);
|
||||
l->dataPos += nbytes;
|
||||
return nbytes;
|
||||
}
|
||||
|
||||
#if DA_FFMPEG_CONST_WRITE_CALLBACK
|
||||
static int WriteData(void *opaque, const uint8_t *buf, int buf_size) {
|
||||
#else
|
||||
static int WriteData(void *opaque, uint8_t *buf, int buf_size) {
|
||||
#endif
|
||||
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 SeekData(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;
|
||||
case AVSEEK_SIZE: {
|
||||
// Special whence for determining filesize without any seek.
|
||||
return l->data.size();
|
||||
} break;
|
||||
}
|
||||
if (newPos < 0) {
|
||||
return -1;
|
||||
}
|
||||
l->dataPos = newPos;
|
||||
return l->dataPos;
|
||||
}
|
||||
};
|
||||
|
||||
Instance::Inner::Inner(QThread *thread)
|
||||
: d(std::make_unique<Private>())
|
||||
, _timer(thread, [=] { process(); }) {
|
||||
moveToThread(thread);
|
||||
}
|
||||
|
||||
Instance::Inner::~Inner() {
|
||||
stop();
|
||||
}
|
||||
|
||||
void Instance::Inner::fail() {
|
||||
stop();
|
||||
if (const auto error = base::take(_error)) {
|
||||
InvokeQueued(this, error);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::start(
|
||||
Webrtc::DeviceResolvedId id,
|
||||
Fn<void(Update)> updated,
|
||||
Fn<void()> error,
|
||||
Fn<void(Chunk)> externalProcessing) {
|
||||
_externalProcessing = std::move(externalProcessing);
|
||||
_updated = std::move(updated);
|
||||
_error = std::move(error);
|
||||
if (_paused) {
|
||||
_paused = false;
|
||||
}
|
||||
|
||||
// Start OpenAL Capture
|
||||
const auto utf = id.isDefault() ? std::string() : id.value.toStdString();
|
||||
d->device = alcCaptureOpenDevice(
|
||||
utf.empty() ? nullptr : utf.c_str(),
|
||||
kCaptureFrequency,
|
||||
AL_FORMAT_MONO16,
|
||||
kCaptureFrequency / 5);
|
||||
if (!d->device) {
|
||||
LOG(("Audio Error: capture device not present!"));
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
alcCaptureStart(d->device);
|
||||
if (ErrorHappened(d->device)) {
|
||||
alcCaptureCloseDevice(d->device);
|
||||
d->device = nullptr;
|
||||
fail();
|
||||
return;
|
||||
} else if (!_externalProcessing) {
|
||||
if (!initializeFFmpeg()) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
}
|
||||
_timer.callEach(50);
|
||||
_captured.clear();
|
||||
_captured.reserve(kCaptureBufferSlice);
|
||||
DEBUG_LOG(("Audio Capture: started!"));
|
||||
}
|
||||
|
||||
bool Instance::Inner::initializeFFmpeg() {
|
||||
// Create encoding context
|
||||
|
||||
d->ioBuffer = (uchar*)av_malloc(FFmpeg::kAVBlockSize);
|
||||
|
||||
d->ioContext = avio_alloc_context(d->ioBuffer, FFmpeg::kAVBlockSize, 1, static_cast<void*>(d.get()), &Private::ReadData, &Private::WriteData, &Private::SeekData);
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
const AVOutputFormat *fmt = nullptr;
|
||||
void *i = nullptr;
|
||||
while ((fmt = av_muxer_iterate(&i))) {
|
||||
if (fmt->name == u"opus"_q) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!fmt) {
|
||||
LOG(("Audio Error: Unable to find opus AVOutputFormat for capture"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((res = avformat_alloc_output_context2(&d->fmtContext, (AVOutputFormat*)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)));
|
||||
return false;
|
||||
}
|
||||
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"));
|
||||
return false;
|
||||
}
|
||||
d->stream = avformat_new_stream(d->fmtContext, d->codec);
|
||||
if (!d->stream) {
|
||||
LOG(("Audio Error: Unable to avformat_new_stream for capture"));
|
||||
return false;
|
||||
}
|
||||
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"));
|
||||
return false;
|
||||
}
|
||||
|
||||
av_opt_set_int(d->codecContext, "refcounted_frames", 1, 0);
|
||||
|
||||
d->codecContext->sample_fmt = AV_SAMPLE_FMT_FLTP;
|
||||
d->codecContext->bit_rate = 32000;
|
||||
d->codecContext->ch_layout = AV_CHANNEL_LAYOUT_MONO;
|
||||
d->channels = d->codecContext->ch_layout.nb_channels;
|
||||
d->codecContext->sample_rate = kCaptureFrequency;
|
||||
|
||||
if (d->fmtContext->oformat->flags & AVFMT_GLOBALHEADER) {
|
||||
d->codecContext->flags |= AV_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)));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Alloc source samples
|
||||
|
||||
d->srcSamples = (d->codecContext->codec->capabilities & AV_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
|
||||
res = swr_alloc_set_opts2(
|
||||
&d->swrContext,
|
||||
&d->codecContext->ch_layout,
|
||||
d->codecContext->sample_fmt,
|
||||
d->codecContext->sample_rate,
|
||||
&d->codecContext->ch_layout,
|
||||
AV_SAMPLE_FMT_S16,
|
||||
d->codecContext->sample_rate,
|
||||
0,
|
||||
nullptr);
|
||||
if (res < 0 || !d->swrContext) {
|
||||
LOG(("Audio Error: Unable to swr_alloc_set_opts2 for capture, error %1, %2").arg(res).arg(av_make_error_string(err, sizeof(err), res)));
|
||||
return false;
|
||||
} else 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)));
|
||||
return false;
|
||||
}
|
||||
d->maxDstSamples = d->srcSamples;
|
||||
if ((res = av_samples_alloc_array_and_samples(&d->dstSamplesData, 0, d->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)));
|
||||
return false;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->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)));
|
||||
return false;
|
||||
}
|
||||
// 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)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Instance::Inner::pause(bool value, Fn<void(Result&&)> callback) {
|
||||
_paused = value;
|
||||
if (!_paused) {
|
||||
return;
|
||||
}
|
||||
if (callback) {
|
||||
callback({
|
||||
.bytes = d->fullSamples ? d->data : QByteArray(),
|
||||
.waveform = (d->fullSamples
|
||||
? CollectWaveform(d->waveform)
|
||||
: VoiceWaveform()),
|
||||
.duration = ((d->fullSamples * crl::time(1000))
|
||||
/ int64(kCaptureFrequency)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::stop(Fn<void(Result&&)> callback) {
|
||||
if (!_timer.isActive()) {
|
||||
return; // in stop() already
|
||||
}
|
||||
_paused = false;
|
||||
_timer.cancel();
|
||||
|
||||
const auto needResult = (callback != nullptr);
|
||||
const auto hadDevice = (d->device != nullptr);
|
||||
if (hadDevice) {
|
||||
alcCaptureStop(d->device);
|
||||
if (d->processing) {
|
||||
Assert(!needResult); // stop in the middle of processing - error.
|
||||
} else {
|
||||
process(); // get last data
|
||||
}
|
||||
alcCaptureCloseDevice(d->device);
|
||||
d->device = nullptr;
|
||||
}
|
||||
|
||||
// Write what is left
|
||||
if (needResult && !_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->channels * sizeof(short), encoded = 0;
|
||||
while (_captured.size() >= encoded + framesize) {
|
||||
if (!processFrame(encoded, framesize)) {
|
||||
break;
|
||||
}
|
||||
encoded += framesize;
|
||||
}
|
||||
// Drain the codec.
|
||||
if (!writeFrame(nullptr) || 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(callback != nullptr)
|
||||
).arg(d->data.size()
|
||||
).arg(d->fullSamples));
|
||||
_captured = QByteArray();
|
||||
|
||||
// Finish stream
|
||||
if (needResult && hadDevice && d->fmtContext) {
|
||||
av_write_trailer(d->fmtContext);
|
||||
}
|
||||
|
||||
QByteArray result = d->fullSamples ? d->data : QByteArray();
|
||||
VoiceWaveform waveform;
|
||||
qint32 samples = d->fullSamples;
|
||||
if (needResult && samples && !d->waveform.isEmpty()) {
|
||||
waveform = CollectWaveform(d->waveform);
|
||||
}
|
||||
if (hadDevice) {
|
||||
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) {
|
||||
callback({
|
||||
.bytes = result,
|
||||
.waveform = waveform,
|
||||
.duration = (samples * crl::time(1000)) / kCaptureFrequency,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::Inner::process() {
|
||||
Expects(!d->processing);
|
||||
|
||||
if (_paused) {
|
||||
return;
|
||||
}
|
||||
|
||||
d->processing = true;
|
||||
const auto guard = gsl::finally([&] { d->processing = false; });
|
||||
|
||||
if (!d->device) {
|
||||
_timer.cancel();
|
||||
return;
|
||||
}
|
||||
ALint samples;
|
||||
alcGetIntegerv(d->device, ALC_CAPTURE_SAMPLES, 1, &samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
if (samples > 0) {
|
||||
// Get samples from OpenAL
|
||||
auto s = _captured.size();
|
||||
auto news = s + static_cast<int>(samples * sizeof(short));
|
||||
if (news / kCaptureBufferSlice > s / kCaptureBufferSlice) {
|
||||
_captured.reserve(((news / kCaptureBufferSlice) + 1) * kCaptureBufferSlice);
|
||||
}
|
||||
_captured.resize(news);
|
||||
alcCaptureSamples(d->device, (ALCvoid *)(_captured.data() + s), samples);
|
||||
if (ErrorHappened(d->device)) {
|
||||
fail();
|
||||
return;
|
||||
} else if (_externalProcessing) {
|
||||
_externalProcessing({
|
||||
.finished = crl::now(),
|
||||
.samples = base::take(_captured),
|
||||
.frequency = kCaptureFrequency,
|
||||
});
|
||||
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 > kCaptureUpdateDelta * kCaptureFrequency / 1000) {
|
||||
_updated(Update{ .samples = samplesFull, .level = d->levelMax });
|
||||
d->lastUpdate = samplesFull;
|
||||
d->levelMax = 0;
|
||||
}
|
||||
// Write frames
|
||||
int32 framesize = d->srcSamples * d->channels * sizeof(short), encoded = 0;
|
||||
while (uint32(_captured.size()) >= encoded + framesize + fadeSamples * sizeof(short)) {
|
||||
if (!processFrame(encoded, framesize)) {
|
||||
return;
|
||||
}
|
||||
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."));
|
||||
}
|
||||
}
|
||||
|
||||
bool 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));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
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->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)));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
d->dstSamplesSize = av_samples_get_buffer_size(0, d->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)));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write audio frame
|
||||
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
|
||||
frame->format = d->codecContext->sample_fmt;
|
||||
av_channel_layout_copy(&frame->ch_layout, &d->codecContext->ch_layout);
|
||||
frame->sample_rate = d->codecContext->sample_rate;
|
||||
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->channels, d->codecContext->sample_fmt, d->dstSamplesData[0], d->dstSamplesSize, 0);
|
||||
|
||||
if (!writeFrame(frame)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
d->fullSamples += samplesCnt;
|
||||
|
||||
av_frame_free(&frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool 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)) {
|
||||
const auto 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()"));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (!packetsWritten) {
|
||||
LOG(("Audio Error: No packets received when EAGAIN was got in avcodec_send_frame()"));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
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)));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!frame) { // drain
|
||||
if ((res = writePackets()) != AVERROR_EOF) {
|
||||
LOG(("Audio Error: not EOF in packets received when draining the codec, result %1").arg(res));
|
||||
fail();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int Instance::Inner::writePackets() {
|
||||
AVPacket *pkt = av_packet_alloc();
|
||||
const auto guard = gsl::finally([&] { av_packet_free(&pkt); });
|
||||
|
||||
int res = 0;
|
||||
char err[AV_ERROR_MAX_STRING_SIZE] = { 0 };
|
||||
|
||||
int written = 0;
|
||||
do {
|
||||
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)));
|
||||
fail();
|
||||
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)));
|
||||
fail();
|
||||
return -1;
|
||||
}
|
||||
|
||||
++written;
|
||||
av_packet_unref(pkt);
|
||||
} while (true);
|
||||
return written;
|
||||
}
|
||||
|
||||
} // namespace Capture
|
||||
} // namespace Media
|
||||
84
Telegram/SourceFiles/media/audio/media_audio_capture.h
Normal file
84
Telegram/SourceFiles/media/audio/media_audio_capture.h
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QThread>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
namespace Media {
|
||||
namespace Capture {
|
||||
|
||||
struct Update {
|
||||
int samples = 0;
|
||||
ushort level = 0;
|
||||
|
||||
bool finished = false;
|
||||
};
|
||||
|
||||
enum class Error : uchar {
|
||||
Other,
|
||||
AudioInit,
|
||||
VideoInit,
|
||||
AudioTimeout,
|
||||
VideoTimeout,
|
||||
Encoding,
|
||||
};
|
||||
|
||||
struct Chunk {
|
||||
crl::time finished = 0;
|
||||
QByteArray samples;
|
||||
int frequency = 0;
|
||||
};
|
||||
|
||||
struct Result;
|
||||
|
||||
void Start();
|
||||
void Finish();
|
||||
|
||||
class Instance final : public QObject {
|
||||
public:
|
||||
Instance();
|
||||
~Instance();
|
||||
|
||||
void check();
|
||||
[[nodiscard]] bool available() const {
|
||||
return _available;
|
||||
}
|
||||
|
||||
[[nodiscard]] rpl::producer<Update, Error> updated() const {
|
||||
return _updates.events();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool started() const {
|
||||
return _started.current();
|
||||
}
|
||||
[[nodiscard]] rpl::producer<bool> startedChanges() const {
|
||||
return _started.changes();
|
||||
}
|
||||
|
||||
void start(Fn<void(Chunk)> externalProcessing = nullptr);
|
||||
void stop(Fn<void(Result&&)> callback = nullptr);
|
||||
void pause(bool value, Fn<void(Result&&)> callback = nullptr);
|
||||
|
||||
private:
|
||||
class Inner;
|
||||
friend class Inner;
|
||||
|
||||
bool _available = false;
|
||||
rpl::variable<bool> _started = false;
|
||||
rpl::event_stream<Update, Error> _updates;
|
||||
QThread _thread;
|
||||
std::unique_ptr<Inner> _inner;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Instance *instance();
|
||||
|
||||
} // namespace Capture
|
||||
} // namespace Media
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Media::Capture {
|
||||
|
||||
struct Result {
|
||||
QByteArray bytes;
|
||||
VoiceWaveform waveform;
|
||||
crl::time duration;
|
||||
bool video = false;
|
||||
};
|
||||
|
||||
} // namespace Media::Capture
|
||||
772
Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.cpp
Normal file
772
Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.cpp
Normal file
@@ -0,0 +1,772 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
|
||||
#include "base/bytes.h"
|
||||
#include "core/file_location.h"
|
||||
#include "ffmpeg/ffmpeg_utility.h"
|
||||
#include "media/media_common.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavfilter/buffersink.h>
|
||||
#include <libavfilter/buffersrc.h>
|
||||
} // extern "C"
|
||||
|
||||
namespace Media {
|
||||
namespace {
|
||||
|
||||
using FFmpeg::AvErrorWrap;
|
||||
using FFmpeg::LogError;
|
||||
|
||||
} // namespace
|
||||
|
||||
int64 AbstractFFMpegLoader::Mul(int64 value, AVRational rational) {
|
||||
return value * rational.num / rational.den;
|
||||
}
|
||||
|
||||
bool AbstractFFMpegLoader::open(crl::time positionMs, float64 speed) {
|
||||
if (!AudioPlayerLoader::openFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ioBuffer = (uchar *)av_malloc(FFmpeg::kAVBlockSize);
|
||||
if (!_data.isEmpty()) {
|
||||
ioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadData, 0, &AbstractFFMpegLoader::SeekData);
|
||||
} else if (!_bytes.empty()) {
|
||||
ioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadBytes, 0, &AbstractFFMpegLoader::SeekBytes);
|
||||
} else {
|
||||
ioContext = avio_alloc_context(ioBuffer, FFmpeg::kAVBlockSize, 0, reinterpret_cast<void *>(this), &AbstractFFMpegLoader::ReadFile, 0, &AbstractFFMpegLoader::SeekFile);
|
||||
}
|
||||
fmtContext = avformat_alloc_context();
|
||||
if (!fmtContext) {
|
||||
LogError(u"avformat_alloc_context"_q);
|
||||
return false;
|
||||
}
|
||||
fmtContext->pb = ioContext;
|
||||
|
||||
if (AvErrorWrap error = avformat_open_input(&fmtContext, 0, 0, 0)) {
|
||||
ioBuffer = nullptr;
|
||||
LogError(u"avformat_open_input"_q, error);
|
||||
return false;
|
||||
}
|
||||
_opened = true;
|
||||
|
||||
if (AvErrorWrap error = avformat_find_stream_info(fmtContext, 0)) {
|
||||
LogError(u"avformat_find_stream_info"_q, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
streamId = av_find_best_stream(fmtContext, AVMEDIA_TYPE_AUDIO, -1, -1, &codec, 0);
|
||||
if (streamId < 0) {
|
||||
FFmpeg::LogError(u"av_find_best_stream"_q, AvErrorWrap(streamId));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto stream = fmtContext->streams[streamId];
|
||||
const auto params = stream->codecpar;
|
||||
_samplesFrequency = params->sample_rate;
|
||||
if (stream->duration != AV_NOPTS_VALUE) {
|
||||
_duration = Mul(stream->duration * 1000, stream->time_base);
|
||||
} else {
|
||||
_duration = Mul(fmtContext->duration * 1000, { 1, AV_TIME_BASE });
|
||||
}
|
||||
_startedAtSample = (positionMs * _samplesFrequency) / 1000LL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
AbstractFFMpegLoader::~AbstractFFMpegLoader() {
|
||||
if (_opened) {
|
||||
avformat_close_input(&fmtContext);
|
||||
}
|
||||
if (ioContext) {
|
||||
av_freep(&ioContext->buffer);
|
||||
av_freep(&ioContext);
|
||||
} else if (ioBuffer) {
|
||||
av_freep(&ioBuffer);
|
||||
}
|
||||
if (fmtContext) avformat_free_context(fmtContext);
|
||||
}
|
||||
|
||||
int AbstractFFMpegLoader::ReadData(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);
|
||||
|
||||
auto nbytes = qMin(l->_data.size() - l->_dataPos, int32(buf_size));
|
||||
if (nbytes <= 0) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
|
||||
memcpy(buf, l->_data.constData() + l->_dataPos, nbytes);
|
||||
l->_dataPos += nbytes;
|
||||
return nbytes;
|
||||
}
|
||||
|
||||
int64_t AbstractFFMpegLoader::SeekData(void *opaque, int64_t offset, int whence) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(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;
|
||||
case AVSEEK_SIZE: {
|
||||
// Special whence for determining filesize without any seek.
|
||||
return l->_data.size();
|
||||
} break;
|
||||
}
|
||||
if (newPos < 0 || newPos > l->_data.size()) {
|
||||
return -1;
|
||||
}
|
||||
l->_dataPos = newPos;
|
||||
return l->_dataPos;
|
||||
}
|
||||
|
||||
int AbstractFFMpegLoader::ReadBytes(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);
|
||||
|
||||
auto nbytes = qMin(static_cast<int>(l->_bytes.size()) - l->_dataPos, buf_size);
|
||||
if (nbytes <= 0) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
|
||||
memcpy(buf, l->_bytes.data() + l->_dataPos, nbytes);
|
||||
l->_dataPos += nbytes;
|
||||
return nbytes;
|
||||
}
|
||||
|
||||
int64_t AbstractFFMpegLoader::SeekBytes(void *opaque, int64_t offset, int whence) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);
|
||||
|
||||
int32 newPos = -1;
|
||||
switch (whence) {
|
||||
case SEEK_SET: newPos = offset; break;
|
||||
case SEEK_CUR: newPos = l->_dataPos + offset; break;
|
||||
case SEEK_END: newPos = static_cast<int>(l->_bytes.size()) + offset; break;
|
||||
case AVSEEK_SIZE:
|
||||
{
|
||||
// Special whence for determining filesize without any seek.
|
||||
return l->_bytes.size();
|
||||
} break;
|
||||
}
|
||||
if (newPos < 0 || newPos > l->_bytes.size()) {
|
||||
return -1;
|
||||
}
|
||||
l->_dataPos = newPos;
|
||||
return l->_dataPos;
|
||||
}
|
||||
|
||||
int AbstractFFMpegLoader::ReadFile(void *opaque, uint8_t *buf, int buf_size) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);
|
||||
int ret = l->_f.read((char *)(buf), buf_size);
|
||||
switch (ret) {
|
||||
case -1: return AVERROR_EXTERNAL;
|
||||
case 0: return AVERROR_EOF;
|
||||
default: return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t AbstractFFMpegLoader::SeekFile(void *opaque, int64_t offset, int whence) {
|
||||
auto l = reinterpret_cast<AbstractFFMpegLoader *>(opaque);
|
||||
|
||||
switch (whence) {
|
||||
case SEEK_SET: return l->_f.seek(offset) ? l->_f.pos() : -1;
|
||||
case SEEK_CUR: return l->_f.seek(l->_f.pos() + offset) ? l->_f.pos() : -1;
|
||||
case SEEK_END: return l->_f.seek(l->_f.size() + offset) ? l->_f.pos() : -1;
|
||||
case AVSEEK_SIZE:
|
||||
{
|
||||
// Special whence for determining filesize without any seek.
|
||||
return l->_f.size();
|
||||
} break;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
AbstractAudioFFMpegLoader::AbstractAudioFFMpegLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer)
|
||||
: AbstractFFMpegLoader(file, data, std::move(buffer))
|
||||
, _frame(FFmpeg::MakeFramePointer()) {
|
||||
}
|
||||
|
||||
void AbstractAudioFFMpegLoader::dropFramesTill(int64 samples) {
|
||||
const auto isAfter = [&](const EnqueuedFrame &frame) {
|
||||
return frame.position > samples;
|
||||
};
|
||||
const auto from = begin(_framesQueued);
|
||||
const auto after = ranges::find_if(_framesQueued, isAfter);
|
||||
if (from == after) {
|
||||
return;
|
||||
}
|
||||
const auto till = after - 1;
|
||||
const auto erasing = till - from;
|
||||
if (erasing > 0) {
|
||||
if (_framesQueuedIndex >= 0) {
|
||||
Assert(_framesQueuedIndex >= erasing);
|
||||
_framesQueuedIndex -= erasing;
|
||||
}
|
||||
_framesQueued.erase(from, till);
|
||||
if (_framesQueued.empty()) {
|
||||
_framesQueuedIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int64 AbstractAudioFFMpegLoader::startReadingQueuedFrames(float64 newSpeed) {
|
||||
changeSpeedFilter(newSpeed);
|
||||
if (_framesQueued.empty()) {
|
||||
_framesQueuedIndex = -1;
|
||||
return -1;
|
||||
}
|
||||
_framesQueuedIndex = 0;
|
||||
return _framesQueued.front().position;
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::initUsingContext(
|
||||
not_null<AVCodecContext*> context,
|
||||
float64 speed) {
|
||||
_swrSrcSampleFormat = context->sample_fmt;
|
||||
const AVChannelLayout mono = AV_CHANNEL_LAYOUT_MONO;
|
||||
const AVChannelLayout stereo = AV_CHANNEL_LAYOUT_STEREO;
|
||||
if (!av_channel_layout_compare(&context->ch_layout, &mono)) {
|
||||
switch (_swrSrcSampleFormat) {
|
||||
case AV_SAMPLE_FMT_U8:
|
||||
case AV_SAMPLE_FMT_U8P:
|
||||
_swrDstSampleFormat = _swrSrcSampleFormat;
|
||||
av_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);
|
||||
_outputChannels = 1;
|
||||
_outputSampleSize = 1;
|
||||
_outputFormat = AL_FORMAT_MONO8;
|
||||
break;
|
||||
case AV_SAMPLE_FMT_S16:
|
||||
case AV_SAMPLE_FMT_S16P:
|
||||
_swrDstSampleFormat = _swrSrcSampleFormat;
|
||||
av_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);
|
||||
_outputChannels = 1;
|
||||
_outputSampleSize = sizeof(uint16);
|
||||
_outputFormat = AL_FORMAT_MONO16;
|
||||
break;
|
||||
}
|
||||
} else if (!av_channel_layout_compare(&context->ch_layout, &stereo)) {
|
||||
switch (_swrSrcSampleFormat) {
|
||||
case AV_SAMPLE_FMT_U8:
|
||||
_swrDstSampleFormat = _swrSrcSampleFormat;
|
||||
av_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);
|
||||
_outputChannels = 2;
|
||||
_outputSampleSize = 2;
|
||||
_outputFormat = AL_FORMAT_STEREO8;
|
||||
break;
|
||||
case AV_SAMPLE_FMT_S16:
|
||||
_swrDstSampleFormat = _swrSrcSampleFormat;
|
||||
av_channel_layout_copy(&_swrDstChannelLayout, &context->ch_layout);
|
||||
_outputChannels = 2;
|
||||
_outputSampleSize = 2 * sizeof(uint16);
|
||||
_outputFormat = AL_FORMAT_STEREO16;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
createSpeedFilter(speed);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto AbstractAudioFFMpegLoader::replaceFrameAndRead(
|
||||
FFmpeg::FramePointer frame)
|
||||
-> ReadResult {
|
||||
_frame = std::move(frame);
|
||||
return readFromReadyFrame();
|
||||
}
|
||||
|
||||
auto AbstractAudioFFMpegLoader::readFromReadyContext(
|
||||
not_null<AVCodecContext*> context)
|
||||
-> ReadResult {
|
||||
if (_filterGraph) {
|
||||
AvErrorWrap error = av_buffersink_get_frame(
|
||||
_filterSink,
|
||||
_filteredFrame.get());
|
||||
if (!error) {
|
||||
if (!_filteredFrame->nb_samples) {
|
||||
return ReadError::Retry;
|
||||
}
|
||||
return bytes::const_span(
|
||||
reinterpret_cast<const bytes::type*>(
|
||||
_filteredFrame->extended_data[0]),
|
||||
_filteredFrame->nb_samples * _outputSampleSize);
|
||||
} else if (error.code() == AVERROR_EOF) {
|
||||
return ReadError::EndOfFile;
|
||||
} else if (error.code() != AVERROR(EAGAIN)) {
|
||||
LogError(u"av_buffersink_get_frame"_q, error);
|
||||
return ReadError::Other;
|
||||
}
|
||||
}
|
||||
using Enqueued = not_null<const EnqueuedFrame*>;
|
||||
const auto queueResult = fillFrameFromQueued();
|
||||
if (queueResult == ReadError::RetryNotQueued) {
|
||||
return ReadError::RetryNotQueued;
|
||||
} else if (const auto enqueued = std::get_if<Enqueued>(&queueResult)) {
|
||||
const auto raw = (*enqueued)->frame.get();
|
||||
Assert(frameHasDesiredFormat(raw));
|
||||
return readOrBufferForFilter(raw, (*enqueued)->samples);
|
||||
}
|
||||
|
||||
const auto queueError = v::get<ReadError>(queueResult);
|
||||
AvErrorWrap error = (queueError == ReadError::EndOfFile)
|
||||
? AVERROR_EOF
|
||||
: avcodec_receive_frame(context, _frame.get());
|
||||
if (!error) {
|
||||
return readFromReadyFrame();
|
||||
}
|
||||
|
||||
if (error.code() == AVERROR_EOF) {
|
||||
enqueueFramesFinished();
|
||||
if (!_filterGraph) {
|
||||
return ReadError::EndOfFile;
|
||||
}
|
||||
AvErrorWrap error = av_buffersrc_add_frame(_filterSrc, nullptr);
|
||||
if (!error) {
|
||||
return ReadError::Retry;
|
||||
}
|
||||
LogError(u"av_buffersrc_add_frame"_q, error);
|
||||
return ReadError::Other;
|
||||
} else if (error.code() != AVERROR(EAGAIN)) {
|
||||
LogError(u"avcodec_receive_frame"_q, error);
|
||||
return ReadError::Other;
|
||||
}
|
||||
return ReadError::Wait;
|
||||
}
|
||||
|
||||
auto AbstractAudioFFMpegLoader::fillFrameFromQueued()
|
||||
-> std::variant<not_null<const EnqueuedFrame*>, ReadError> {
|
||||
if (_framesQueuedIndex == _framesQueued.size()) {
|
||||
_framesQueuedIndex = -1;
|
||||
return ReadError::RetryNotQueued;
|
||||
} else if (_framesQueuedIndex < 0) {
|
||||
return ReadError::Wait;
|
||||
}
|
||||
const auto &queued = _framesQueued[_framesQueuedIndex];
|
||||
++_framesQueuedIndex;
|
||||
|
||||
if (!queued.frame) {
|
||||
return ReadError::EndOfFile;
|
||||
}
|
||||
return &queued;
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::frameHasDesiredFormat(
|
||||
not_null<AVFrame*> frame) const {
|
||||
return true
|
||||
&& (frame->format == _swrDstSampleFormat)
|
||||
&& (frame->sample_rate == _swrDstRate)
|
||||
&& !av_channel_layout_compare(
|
||||
&frame->ch_layout,
|
||||
&_swrDstChannelLayout);
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::initResampleForFrame() {
|
||||
if (!_frame->ch_layout.nb_channels) {
|
||||
LOG(("Audio Error: "
|
||||
"Unknown channel layout for frame in file '%1', "
|
||||
"data size '%2'"
|
||||
).arg(_file.name()
|
||||
).arg(_data.size()
|
||||
));
|
||||
return false;
|
||||
} else if (_frame->format == -1) {
|
||||
LOG(("Audio Error: "
|
||||
"Unknown frame format in file '%1', data size '%2'"
|
||||
).arg(_file.name()
|
||||
).arg(_data.size()
|
||||
));
|
||||
return false;
|
||||
} else if (_swrContext) {
|
||||
if (true
|
||||
&& (_frame->format == _swrSrcSampleFormat)
|
||||
&& (_frame->sample_rate == _swrSrcRate)
|
||||
&& !av_channel_layout_compare(
|
||||
&_frame->ch_layout,
|
||||
&_swrSrcChannelLayout)) {
|
||||
return true;
|
||||
}
|
||||
swr_close(_swrContext);
|
||||
}
|
||||
|
||||
_swrSrcSampleFormat = static_cast<AVSampleFormat>(_frame->format);
|
||||
av_channel_layout_copy(&_swrSrcChannelLayout, &_frame->ch_layout);
|
||||
_swrSrcRate = _frame->sample_rate;
|
||||
return initResampleUsingFormat();
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::initResampleUsingFormat() {
|
||||
auto error = swr_alloc_set_opts2(
|
||||
&_swrContext,
|
||||
&_swrDstChannelLayout,
|
||||
_swrDstSampleFormat,
|
||||
_swrDstRate,
|
||||
&_swrSrcChannelLayout,
|
||||
_swrSrcSampleFormat,
|
||||
_swrSrcRate,
|
||||
0,
|
||||
nullptr);
|
||||
if (error || !_swrContext) {
|
||||
LogError(u"swr_alloc_set_opts2"_q, error);
|
||||
return false;
|
||||
} else if (AvErrorWrap error = swr_init(_swrContext)) {
|
||||
LogError(u"swr_init"_q, error);
|
||||
return false;
|
||||
}
|
||||
_resampledFrame = nullptr;
|
||||
_resampledFrameCapacity = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::ensureResampleSpaceAvailable(int samples) {
|
||||
const auto enlarge = (_resampledFrameCapacity < samples);
|
||||
if (!_resampledFrame) {
|
||||
_resampledFrame = FFmpeg::MakeFramePointer();
|
||||
} else if (enlarge || !av_frame_is_writable(_resampledFrame.get())) {
|
||||
av_frame_unref(_resampledFrame.get());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
const auto allocate = std::max(samples, int(av_rescale_rnd(
|
||||
FFmpeg::kAVBlockSize / _outputSampleSize,
|
||||
_swrDstRate,
|
||||
_swrSrcRate,
|
||||
AV_ROUND_UP)));
|
||||
_resampledFrame->sample_rate = _swrDstRate;
|
||||
_resampledFrame->format = _swrDstSampleFormat;
|
||||
av_channel_layout_copy(
|
||||
&_resampledFrame->ch_layout,
|
||||
&_swrDstChannelLayout);
|
||||
_resampledFrame->nb_samples = allocate;
|
||||
if (AvErrorWrap error = av_frame_get_buffer(_resampledFrame.get(), 0)) {
|
||||
LogError(u"av_frame_get_buffer"_q, error);
|
||||
return false;
|
||||
}
|
||||
_resampledFrameCapacity = allocate;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AbstractAudioFFMpegLoader::changeSpeedFilter(float64 speed) {
|
||||
speed = std::clamp(speed, kSpeedMin, kSpeedMax);
|
||||
if (EqualSpeeds(_filterSpeed, speed)) {
|
||||
return false;
|
||||
}
|
||||
avfilter_graph_free(&_filterGraph);
|
||||
const auto guard = gsl::finally([&] {
|
||||
if (!_filterGraph) {
|
||||
_filteredFrame = nullptr;
|
||||
_filterSpeed = 1.;
|
||||
}
|
||||
});
|
||||
createSpeedFilter(speed);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AbstractAudioFFMpegLoader::createSpeedFilter(float64 speed) {
|
||||
Expects(!_filterGraph);
|
||||
|
||||
if (EqualSpeeds(speed, 1.)) {
|
||||
return;
|
||||
}
|
||||
const auto abuffer = avfilter_get_by_name("abuffer");
|
||||
const auto abuffersink = avfilter_get_by_name("abuffersink");
|
||||
const auto atempo = avfilter_get_by_name("atempo");
|
||||
if (!abuffer || !abuffersink || !atempo) {
|
||||
LOG(("FFmpeg Error: Could not find abuffer / abuffersink /atempo."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto graph = avfilter_graph_alloc();
|
||||
if (!graph) {
|
||||
LOG(("FFmpeg Error: Unable to create filter graph."));
|
||||
return;
|
||||
}
|
||||
const auto guard = gsl::finally([&] {
|
||||
avfilter_graph_free(&graph);
|
||||
});
|
||||
|
||||
_filterSrc = avfilter_graph_alloc_filter(graph, abuffer, "src");
|
||||
_atempo = avfilter_graph_alloc_filter(graph, atempo, "atempo");
|
||||
_filterSink = avfilter_graph_alloc_filter(graph, abuffersink, "sink");
|
||||
if (!_filterSrc || !atempo || !_filterSink) {
|
||||
LOG(("FFmpeg Error: "
|
||||
"Could not allocate abuffer / abuffersink /atempo."));
|
||||
return;
|
||||
}
|
||||
|
||||
char layout[64] = { 0 };
|
||||
av_channel_layout_describe(
|
||||
&_swrDstChannelLayout,
|
||||
layout,
|
||||
sizeof(layout));
|
||||
|
||||
av_opt_set(
|
||||
_filterSrc,
|
||||
"channel_layout",
|
||||
layout,
|
||||
AV_OPT_SEARCH_CHILDREN);
|
||||
av_opt_set_sample_fmt(
|
||||
_filterSrc,
|
||||
"sample_fmt",
|
||||
_swrDstSampleFormat,
|
||||
AV_OPT_SEARCH_CHILDREN);
|
||||
av_opt_set_q(
|
||||
_filterSrc,
|
||||
"time_base",
|
||||
AVRational{ 1, _swrDstRate },
|
||||
AV_OPT_SEARCH_CHILDREN);
|
||||
av_opt_set_int(
|
||||
_filterSrc,
|
||||
"sample_rate",
|
||||
_swrDstRate,
|
||||
AV_OPT_SEARCH_CHILDREN);
|
||||
av_opt_set_double(
|
||||
_atempo,
|
||||
"tempo",
|
||||
speed,
|
||||
AV_OPT_SEARCH_CHILDREN);
|
||||
|
||||
AvErrorWrap error = 0;
|
||||
if ((error = avfilter_init_str(_filterSrc, nullptr))) {
|
||||
LogError(u"avfilter_init_str(src)"_q, error);
|
||||
return;
|
||||
} else if ((error = avfilter_init_str(_atempo, nullptr))) {
|
||||
LogError(u"avfilter_init_str(atempo)"_q, error);
|
||||
avfilter_graph_free(&graph);
|
||||
return;
|
||||
} else if ((error = avfilter_init_str(_filterSink, nullptr))) {
|
||||
LogError(u"avfilter_init_str(sink)"_q, error);
|
||||
avfilter_graph_free(&graph);
|
||||
return;
|
||||
} else if ((error = avfilter_link(_filterSrc, 0, _atempo, 0))) {
|
||||
LogError(u"avfilter_link(src->atempo)"_q, error);
|
||||
avfilter_graph_free(&graph);
|
||||
return;
|
||||
} else if ((error = avfilter_link(_atempo, 0, _filterSink, 0))) {
|
||||
LogError(u"avfilter_link(atempo->sink)"_q, error);
|
||||
avfilter_graph_free(&graph);
|
||||
return;
|
||||
} else if ((error = avfilter_graph_config(graph, nullptr))) {
|
||||
LogError("avfilter_link(atempo->sink)"_q, error);
|
||||
avfilter_graph_free(&graph);
|
||||
return;
|
||||
}
|
||||
_filterGraph = base::take(graph);
|
||||
_filteredFrame = FFmpeg::MakeFramePointer();
|
||||
_filterSpeed = speed;
|
||||
}
|
||||
|
||||
void AbstractAudioFFMpegLoader::enqueueNormalFrame(
|
||||
not_null<AVFrame*> frame,
|
||||
int64 samples) {
|
||||
if (_framesQueuedIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
if (!samples) {
|
||||
samples = frame->nb_samples;
|
||||
}
|
||||
_framesQueued.push_back({
|
||||
.position = startedAtSample() + _framesQueuedSamples,
|
||||
.samples = samples,
|
||||
.frame = FFmpeg::DuplicateFramePointer(frame),
|
||||
});
|
||||
_framesQueuedSamples += samples;
|
||||
}
|
||||
|
||||
void AbstractAudioFFMpegLoader::enqueueFramesFinished() {
|
||||
if (_framesQueuedIndex >= 0) {
|
||||
return;
|
||||
}
|
||||
_framesQueued.push_back({
|
||||
.position = startedAtSample() + _framesQueuedSamples,
|
||||
});
|
||||
}
|
||||
|
||||
auto AbstractAudioFFMpegLoader::readFromReadyFrame()
|
||||
-> ReadResult {
|
||||
const auto raw = _frame.get();
|
||||
if (frameHasDesiredFormat(raw)) {
|
||||
if (!raw->nb_samples) {
|
||||
return ReadError::Retry;
|
||||
}
|
||||
return readOrBufferForFilter(raw, raw->nb_samples);
|
||||
} else if (!initResampleForFrame()) {
|
||||
return ReadError::Other;
|
||||
}
|
||||
|
||||
const auto maxSamples = av_rescale_rnd(
|
||||
swr_get_delay(_swrContext, _swrSrcRate) + _frame->nb_samples,
|
||||
_swrDstRate,
|
||||
_swrSrcRate,
|
||||
AV_ROUND_UP);
|
||||
if (!ensureResampleSpaceAvailable(maxSamples)) {
|
||||
return ReadError::Other;
|
||||
}
|
||||
const auto samples = swr_convert(
|
||||
_swrContext,
|
||||
(uint8_t**)_resampledFrame->extended_data,
|
||||
maxSamples,
|
||||
(const uint8_t **)_frame->extended_data,
|
||||
_frame->nb_samples);
|
||||
if (AvErrorWrap error = samples) {
|
||||
LogError(u"swr_convert"_q, error);
|
||||
return ReadError::Other;
|
||||
} else if (!samples) {
|
||||
return ReadError::Retry;
|
||||
}
|
||||
return readOrBufferForFilter(_resampledFrame.get(), samples);
|
||||
}
|
||||
|
||||
auto AbstractAudioFFMpegLoader::readOrBufferForFilter(
|
||||
not_null<AVFrame*> frame,
|
||||
int64 samplesOverride)
|
||||
-> ReadResult {
|
||||
enqueueNormalFrame(frame, samplesOverride);
|
||||
|
||||
const auto was = frame->nb_samples;
|
||||
frame->nb_samples = samplesOverride;
|
||||
const auto guard = gsl::finally([&] {
|
||||
frame->nb_samples = was;
|
||||
});
|
||||
|
||||
if (!_filterGraph) {
|
||||
return bytes::const_span(
|
||||
reinterpret_cast<const bytes::type*>(frame->extended_data[0]),
|
||||
frame->nb_samples * _outputSampleSize);
|
||||
}
|
||||
AvErrorWrap error = av_buffersrc_add_frame_flags(
|
||||
_filterSrc,
|
||||
frame,
|
||||
AV_BUFFERSRC_FLAG_KEEP_REF);
|
||||
if (error) {
|
||||
LogError(u"av_buffersrc_add_frame_flags"_q, error);
|
||||
return ReadError::Other;
|
||||
}
|
||||
return ReadError::Retry;
|
||||
}
|
||||
|
||||
AbstractAudioFFMpegLoader::~AbstractAudioFFMpegLoader() {
|
||||
if (_filterGraph) {
|
||||
avfilter_graph_free(&_filterGraph);
|
||||
}
|
||||
if (_swrContext) {
|
||||
swr_free(&_swrContext);
|
||||
}
|
||||
}
|
||||
|
||||
FFMpegLoader::FFMpegLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer)
|
||||
: AbstractAudioFFMpegLoader(file, data, std::move(buffer)) {
|
||||
}
|
||||
|
||||
bool FFMpegLoader::open(crl::time positionMs, float64 speed) {
|
||||
return AbstractFFMpegLoader::open(positionMs)
|
||||
&& openCodecContext()
|
||||
&& initUsingContext(_codecContext, speed)
|
||||
&& seekTo(positionMs);
|
||||
}
|
||||
|
||||
bool FFMpegLoader::openCodecContext() {
|
||||
_codecContext = avcodec_alloc_context3(nullptr);
|
||||
if (!_codecContext) {
|
||||
LOG(("Audio Error: "
|
||||
"Unable to avcodec_alloc_context3 for file '%1', data size '%2'"
|
||||
).arg(_file.name()
|
||||
).arg(_data.size()
|
||||
));
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto stream = fmtContext->streams[streamId];
|
||||
AvErrorWrap error = avcodec_parameters_to_context(
|
||||
_codecContext,
|
||||
stream->codecpar);
|
||||
if (error) {
|
||||
LogError(u"avcodec_parameters_to_context"_q, error);
|
||||
return false;
|
||||
}
|
||||
_codecContext->pkt_timebase = stream->time_base;
|
||||
av_opt_set_int(_codecContext, "refcounted_frames", 1, 0);
|
||||
|
||||
if (AvErrorWrap error = avcodec_open2(_codecContext, codec, 0)) {
|
||||
LogError(u"avcodec_open2"_q, error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FFMpegLoader::seekTo(crl::time positionMs) {
|
||||
if (positionMs) {
|
||||
const auto stream = fmtContext->streams[streamId];
|
||||
const auto timeBase = stream->time_base;
|
||||
const auto timeStamp = (positionMs * timeBase.den)
|
||||
/ (1000LL * timeBase.num);
|
||||
const auto flags1 = AVSEEK_FLAG_ANY;
|
||||
if (av_seek_frame(fmtContext, streamId, timeStamp, flags1) < 0) {
|
||||
const auto flags2 = 0;
|
||||
if (av_seek_frame(fmtContext, streamId, timeStamp, flags2) < 0) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
FFMpegLoader::ReadResult FFMpegLoader::readMore() {
|
||||
if (_readTillEnd) {
|
||||
return ReadError::EndOfFile;
|
||||
}
|
||||
const auto readResult = readFromReadyContext(_codecContext);
|
||||
if (readResult != ReadError::Wait) {
|
||||
if (readResult == ReadError::EndOfFile) {
|
||||
_readTillEnd = true;
|
||||
}
|
||||
return readResult;
|
||||
}
|
||||
|
||||
if (AvErrorWrap error = av_read_frame(fmtContext, &_packet)) {
|
||||
if (error.code() != AVERROR_EOF) {
|
||||
LogError(u"av_read_frame"_q, error);
|
||||
return ReadError::Other;
|
||||
}
|
||||
error = avcodec_send_packet(_codecContext, nullptr); // drain
|
||||
if (!error) {
|
||||
return ReadError::Retry;
|
||||
}
|
||||
LogError(u"avcodec_send_packet"_q, error);
|
||||
return ReadError::Other;
|
||||
}
|
||||
|
||||
if (_packet.stream_index == streamId) {
|
||||
AvErrorWrap error = avcodec_send_packet(_codecContext, &_packet);
|
||||
if (error) {
|
||||
av_packet_unref(&_packet);
|
||||
LogError(u"avcodec_send_packet"_q, error);
|
||||
// There is a sample voice message where skipping such packet
|
||||
// results in a crash (read_access to nullptr) in swr_convert().
|
||||
//if (error.code() == AVERROR_INVALIDDATA) {
|
||||
// return ReadResult::Retry; // try to skip bad packet
|
||||
//}
|
||||
return ReadError::Other;
|
||||
}
|
||||
}
|
||||
av_packet_unref(&_packet);
|
||||
return ReadError::Retry;
|
||||
}
|
||||
|
||||
FFMpegLoader::~FFMpegLoader() {
|
||||
if (_codecContext) {
|
||||
avcodec_free_context(&_codecContext);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Media
|
||||
197
Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.h
Normal file
197
Telegram/SourceFiles/media/audio/media_audio_ffmpeg_loader.h
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/audio/media_audio_loader.h"
|
||||
#include "media/streaming/media_streaming_utility.h"
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <libavfilter/avfilter.h>
|
||||
} // extern "C"
|
||||
|
||||
#include <al.h>
|
||||
|
||||
namespace Core {
|
||||
class FileLocation;
|
||||
} // namespace Core
|
||||
|
||||
namespace Media {
|
||||
|
||||
class AbstractFFMpegLoader : public AudioPlayerLoader {
|
||||
public:
|
||||
AbstractFFMpegLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer)
|
||||
: AudioPlayerLoader(file, data, std::move(buffer)) {
|
||||
}
|
||||
|
||||
bool open(crl::time positionMs, float64 speed = 1.) override;
|
||||
|
||||
crl::time duration() override {
|
||||
return _duration;
|
||||
}
|
||||
void overrideDuration(int64 startedAtSample, crl::time duration) {
|
||||
_startedAtSample = startedAtSample;
|
||||
_duration = duration;
|
||||
}
|
||||
|
||||
int samplesFrequency() override {
|
||||
return _samplesFrequency;
|
||||
}
|
||||
|
||||
[[nodiscard]] int64 startedAtSample() const {
|
||||
return _startedAtSample;
|
||||
}
|
||||
|
||||
~AbstractFFMpegLoader();
|
||||
|
||||
protected:
|
||||
static int64 Mul(int64 value, AVRational rational);
|
||||
|
||||
int _samplesFrequency = Media::Player::kDefaultFrequency;
|
||||
int64 _startedAtSample = 0;
|
||||
crl::time _duration = 0;
|
||||
|
||||
uchar *ioBuffer = nullptr;
|
||||
AVIOContext *ioContext = nullptr;
|
||||
AVFormatContext *fmtContext = nullptr;
|
||||
const AVCodec *codec = nullptr;
|
||||
int32 streamId = 0;
|
||||
|
||||
bool _opened = false;
|
||||
|
||||
private:
|
||||
static int ReadData(void *opaque, uint8_t *buf, int buf_size);
|
||||
static int64_t SeekData(void *opaque, int64_t offset, int whence);
|
||||
static int ReadBytes(void *opaque, uint8_t *buf, int buf_size);
|
||||
static int64_t SeekBytes(void *opaque, int64_t offset, int whence);
|
||||
static int ReadFile(void *opaque, uint8_t *buf, int buf_size);
|
||||
static int64_t SeekFile(void *opaque, int64_t offset, int whence);
|
||||
|
||||
};
|
||||
|
||||
class AbstractAudioFFMpegLoader : public AbstractFFMpegLoader {
|
||||
public:
|
||||
AbstractAudioFFMpegLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer);
|
||||
|
||||
void dropFramesTill(int64 samples) override;
|
||||
int64 startReadingQueuedFrames(float64 newSpeed) override;
|
||||
|
||||
int samplesFrequency() override {
|
||||
return _swrDstRate;
|
||||
}
|
||||
|
||||
int sampleSize() override {
|
||||
return _outputSampleSize;
|
||||
}
|
||||
|
||||
int format() override {
|
||||
return _outputFormat;
|
||||
}
|
||||
|
||||
~AbstractAudioFFMpegLoader();
|
||||
|
||||
protected:
|
||||
bool initUsingContext(not_null<AVCodecContext*> context, float64 speed);
|
||||
[[nodiscard]] ReadResult readFromReadyContext(
|
||||
not_null<AVCodecContext*> context);
|
||||
|
||||
// Streaming player provides the first frame to the ChildFFMpegLoader
|
||||
// so we replace our allocated frame with the one provided.
|
||||
[[nodiscard]] ReadResult replaceFrameAndRead(FFmpeg::FramePointer frame);
|
||||
|
||||
private:
|
||||
struct EnqueuedFrame {
|
||||
int64 position = 0;
|
||||
int64 samples = 0;
|
||||
FFmpeg::FramePointer frame;
|
||||
};
|
||||
[[nodiscard]] ReadResult readFromReadyFrame();
|
||||
[[nodiscard]] ReadResult readOrBufferForFilter(
|
||||
not_null<AVFrame*> frame,
|
||||
int64 samplesOverride);
|
||||
bool frameHasDesiredFormat(not_null<AVFrame*> frame) const;
|
||||
bool initResampleForFrame();
|
||||
bool initResampleUsingFormat();
|
||||
bool ensureResampleSpaceAvailable(int samples);
|
||||
|
||||
bool changeSpeedFilter(float64 speed);
|
||||
void createSpeedFilter(float64 speed);
|
||||
|
||||
void enqueueNormalFrame(
|
||||
not_null<AVFrame*> frame,
|
||||
int64 samples = 0);
|
||||
void enqueueFramesFinished();
|
||||
[[nodiscard]] auto fillFrameFromQueued()
|
||||
-> std::variant<not_null<const EnqueuedFrame*>, ReadError>;
|
||||
|
||||
FFmpeg::FramePointer _frame;
|
||||
FFmpeg::FramePointer _resampledFrame;
|
||||
FFmpeg::FramePointer _filteredFrame;
|
||||
int _resampledFrameCapacity = 0;
|
||||
|
||||
int64 _framesQueuedSamples = 0;
|
||||
std::deque<EnqueuedFrame> _framesQueued;
|
||||
int _framesQueuedIndex = -1;
|
||||
|
||||
int _outputFormat = AL_FORMAT_STEREO16;
|
||||
int _outputChannels = 2;
|
||||
int _outputSampleSize = 2 * sizeof(uint16);
|
||||
|
||||
SwrContext *_swrContext = nullptr;
|
||||
|
||||
int _swrSrcRate = 0;
|
||||
AVSampleFormat _swrSrcSampleFormat = AV_SAMPLE_FMT_NONE;
|
||||
|
||||
const int _swrDstRate = Media::Player::kDefaultFrequency;
|
||||
AVSampleFormat _swrDstSampleFormat = AV_SAMPLE_FMT_S16;
|
||||
|
||||
AVChannelLayout _swrSrcChannelLayout = AV_CHANNEL_LAYOUT_STEREO;
|
||||
AVChannelLayout _swrDstChannelLayout = AV_CHANNEL_LAYOUT_STEREO;
|
||||
|
||||
AVFilterGraph *_filterGraph = nullptr;
|
||||
float64 _filterSpeed = 1.;
|
||||
AVFilterContext *_filterSrc = nullptr;
|
||||
AVFilterContext *_atempo = nullptr;
|
||||
AVFilterContext *_filterSink = nullptr;
|
||||
|
||||
};
|
||||
|
||||
class FFMpegLoader : public AbstractAudioFFMpegLoader {
|
||||
public:
|
||||
FFMpegLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer);
|
||||
|
||||
bool open(crl::time positionMs, float64 speed = 1.) override;
|
||||
|
||||
ReadResult readMore() override;
|
||||
|
||||
~FFMpegLoader();
|
||||
|
||||
private:
|
||||
bool openCodecContext();
|
||||
bool seekTo(crl::time positionMs);
|
||||
|
||||
AVCodecContext *_codecContext = nullptr;
|
||||
AVPacket _packet;
|
||||
bool _readTillEnd = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
97
Telegram/SourceFiles/media/audio/media_audio_loader.cpp
Normal file
97
Telegram/SourceFiles/media/audio/media_audio_loader.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_loader.h"
|
||||
|
||||
namespace Media {
|
||||
|
||||
AudioPlayerLoader::AudioPlayerLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer)
|
||||
: _file(file)
|
||||
, _data(data)
|
||||
, _bytes(std::move(buffer)) {
|
||||
}
|
||||
|
||||
AudioPlayerLoader::~AudioPlayerLoader() {
|
||||
if (_access) {
|
||||
_file.accessDisable();
|
||||
_access = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioPlayerLoader::check(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data) {
|
||||
return (this->_file == file) && (this->_data.size() == data.size());
|
||||
}
|
||||
|
||||
void AudioPlayerLoader::saveDecodedSamples(not_null<QByteArray*> samples) {
|
||||
Expects(_savedSamples.isEmpty());
|
||||
Expects(!_holdsSavedSamples);
|
||||
|
||||
samples->swap(_savedSamples);
|
||||
_holdsSavedSamples = true;
|
||||
}
|
||||
|
||||
void AudioPlayerLoader::takeSavedDecodedSamples(
|
||||
not_null<QByteArray*> samples) {
|
||||
Expects(samples->isEmpty());
|
||||
Expects(_holdsSavedSamples);
|
||||
|
||||
samples->swap(_savedSamples);
|
||||
_holdsSavedSamples = false;
|
||||
}
|
||||
|
||||
bool AudioPlayerLoader::holdsSavedDecodedSamples() const {
|
||||
return _holdsSavedSamples;
|
||||
}
|
||||
|
||||
void AudioPlayerLoader::dropDecodedSamples() {
|
||||
_savedSamples = {};
|
||||
_holdsSavedSamples = false;
|
||||
}
|
||||
|
||||
int AudioPlayerLoader::bytesPerBuffer() {
|
||||
if (!_bytesPerBuffer) {
|
||||
_bytesPerBuffer = samplesFrequency() * sampleSize();
|
||||
}
|
||||
return _bytesPerBuffer;
|
||||
}
|
||||
|
||||
bool AudioPlayerLoader::openFile() {
|
||||
if (_data.isEmpty() && _bytes.empty()) {
|
||||
if (_f.isOpen()) _f.close();
|
||||
if (!_access) {
|
||||
if (!_file.accessEnable()) {
|
||||
LOG(("Audio Error: could not open file access '%1', "
|
||||
"data size '%2', error %3, %4"
|
||||
).arg(_file.name()
|
||||
).arg(_data.size()
|
||||
).arg(_f.error()
|
||||
).arg(_f.errorString()));
|
||||
return false;
|
||||
}
|
||||
_access = true;
|
||||
}
|
||||
_f.setFileName(_file.name());
|
||||
if (!_f.open(QIODevice::ReadOnly)) {
|
||||
LOG(("Audio Error: could not open file '%1', "
|
||||
"data size '%2', error %3, %4"
|
||||
).arg(_file.name()
|
||||
).arg(_data.size()
|
||||
).arg(_f.error()
|
||||
).arg(_f.errorString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_dataPos = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Media
|
||||
87
Telegram/SourceFiles/media/audio/media_audio_loader.h
Normal file
87
Telegram/SourceFiles/media/audio/media_audio_loader.h
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/bytes.h"
|
||||
#include "core/file_location.h"
|
||||
#include "media/streaming/media_streaming_utility.h"
|
||||
|
||||
namespace Media {
|
||||
|
||||
class AudioPlayerLoader {
|
||||
public:
|
||||
AudioPlayerLoader(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data,
|
||||
bytes::vector &&buffer);
|
||||
virtual ~AudioPlayerLoader();
|
||||
|
||||
virtual bool check(
|
||||
const Core::FileLocation &file,
|
||||
const QByteArray &data);
|
||||
|
||||
virtual bool open(crl::time positionMs, float64 speed = 1.) = 0;
|
||||
virtual crl::time duration() = 0;
|
||||
virtual int samplesFrequency() = 0;
|
||||
virtual int sampleSize() = 0;
|
||||
virtual int format() = 0;
|
||||
|
||||
virtual void dropFramesTill(int64 samples) {
|
||||
}
|
||||
[[nodiscard]] virtual int64 startReadingQueuedFrames(float64 newSpeed) {
|
||||
Unexpected(
|
||||
"startReadingQueuedFrames() on not AbstractAudioFFMpegLoader");
|
||||
}
|
||||
|
||||
[[nodiscard]] int bytesPerBuffer();
|
||||
|
||||
enum class ReadError {
|
||||
Other,
|
||||
Retry,
|
||||
RetryNotQueued,
|
||||
Wait,
|
||||
EndOfFile,
|
||||
};
|
||||
using ReadResult = std::variant<bytes::const_span, ReadError>;
|
||||
[[nodiscard]] virtual ReadResult readMore() = 0;
|
||||
|
||||
virtual void enqueuePackets(std::deque<FFmpeg::Packet> &&packets) {
|
||||
Unexpected("enqueuePackets() call on not ChildFFMpegLoader.");
|
||||
}
|
||||
virtual void setForceToBuffer(bool force) {
|
||||
Unexpected("setForceToBuffer() call on not ChildFFMpegLoader.");
|
||||
}
|
||||
virtual bool forceToBuffer() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
void saveDecodedSamples(not_null<QByteArray*> samples);
|
||||
void takeSavedDecodedSamples(not_null<QByteArray*> samples);
|
||||
bool holdsSavedDecodedSamples() const;
|
||||
void dropDecodedSamples();
|
||||
|
||||
protected:
|
||||
Core::FileLocation _file;
|
||||
bool _access = false;
|
||||
QByteArray _data;
|
||||
bytes::vector _bytes;
|
||||
|
||||
QFile _f;
|
||||
int _dataPos = 0;
|
||||
|
||||
bool openFile();
|
||||
|
||||
private:
|
||||
QByteArray _savedSamples;
|
||||
bool _holdsSavedSamples = false;
|
||||
|
||||
int _bytesPerBuffer = 0;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
587
Telegram/SourceFiles/media/audio/media_audio_loaders.cpp
Normal file
587
Telegram/SourceFiles/media/audio/media_audio_loaders.cpp
Normal file
@@ -0,0 +1,587 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_loaders.h"
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
#include "media/audio/media_child_ffmpeg_loader.h"
|
||||
#include "media/media_common.h"
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
Loaders::Loaders(QThread *thread)
|
||||
: _fromExternalNotify([=] { videoSoundAdded(); }) {
|
||||
moveToThread(thread);
|
||||
_fromExternalNotify.moveToThread(thread);
|
||||
connect(thread, SIGNAL(started()), this, SLOT(onInit()));
|
||||
connect(thread, SIGNAL(finished()), this, SLOT(deleteLater()));
|
||||
}
|
||||
|
||||
void Loaders::feedFromExternal(ExternalSoundPart &&part) {
|
||||
auto invoke = false;
|
||||
{
|
||||
QMutexLocker lock(&_fromExternalMutex);
|
||||
invoke = _fromExternalQueues.empty()
|
||||
&& _fromExternalForceToBuffer.empty();
|
||||
auto &queue = _fromExternalQueues[part.audio];
|
||||
queue.insert(
|
||||
end(queue),
|
||||
std::make_move_iterator(part.packets.begin()),
|
||||
std::make_move_iterator(part.packets.end()));
|
||||
}
|
||||
if (invoke) {
|
||||
_fromExternalNotify.call();
|
||||
}
|
||||
}
|
||||
|
||||
void Loaders::forceToBufferExternal(const AudioMsgId &audioId) {
|
||||
auto invoke = false;
|
||||
{
|
||||
QMutexLocker lock(&_fromExternalMutex);
|
||||
invoke = _fromExternalQueues.empty()
|
||||
&& _fromExternalForceToBuffer.empty();
|
||||
_fromExternalForceToBuffer.emplace(audioId);
|
||||
}
|
||||
if (invoke) {
|
||||
_fromExternalNotify.call();
|
||||
}
|
||||
}
|
||||
|
||||
void Loaders::videoSoundAdded() {
|
||||
auto queues = decltype(_fromExternalQueues)();
|
||||
auto forces = decltype(_fromExternalForceToBuffer)();
|
||||
{
|
||||
QMutexLocker lock(&_fromExternalMutex);
|
||||
queues = base::take(_fromExternalQueues);
|
||||
forces = base::take(_fromExternalForceToBuffer);
|
||||
}
|
||||
for (const auto &audioId : forces) {
|
||||
const auto tryLoader = [&](const auto &id, auto &loader) {
|
||||
if (audioId == id && loader) {
|
||||
loader->setForceToBuffer(true);
|
||||
if (loader->holdsSavedDecodedSamples()
|
||||
&& !queues.contains(audioId)) {
|
||||
loadData(audioId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
tryLoader(_audio, _audioLoader)
|
||||
|| tryLoader(_song, _songLoader)
|
||||
|| tryLoader(_video, _videoLoader);
|
||||
}
|
||||
for (auto &pair : queues) {
|
||||
const auto audioId = pair.first;
|
||||
auto &packets = pair.second;
|
||||
const auto tryLoader = [&](const auto &id, auto &loader) {
|
||||
if (id == audioId && loader) {
|
||||
loader->enqueuePackets(std::move(packets));
|
||||
if (loader->holdsSavedDecodedSamples()) {
|
||||
loadData(audioId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
tryLoader(_audio, _audioLoader)
|
||||
|| tryLoader(_song, _songLoader)
|
||||
|| tryLoader(_video, _videoLoader);
|
||||
}
|
||||
}
|
||||
|
||||
void Loaders::onInit() {
|
||||
}
|
||||
|
||||
void Loaders::onStart(const AudioMsgId &audio, qint64 positionMs) {
|
||||
auto type = audio.type();
|
||||
clear(type);
|
||||
{
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
if (!mixer()) return;
|
||||
|
||||
auto track = mixer()->trackForType(type);
|
||||
if (!track) return;
|
||||
|
||||
track->loading = true;
|
||||
}
|
||||
|
||||
loadData(audio, positionMs);
|
||||
}
|
||||
|
||||
AudioMsgId Loaders::clear(AudioMsgId::Type type) {
|
||||
AudioMsgId result;
|
||||
switch (type) {
|
||||
case AudioMsgId::Type::Voice:
|
||||
std::swap(result, _audio);
|
||||
_audioLoader = nullptr;
|
||||
break;
|
||||
case AudioMsgId::Type::Song:
|
||||
std::swap(result, _song);
|
||||
_songLoader = nullptr;
|
||||
break;
|
||||
case AudioMsgId::Type::Video:
|
||||
std::swap(result, _video);
|
||||
_videoLoader = nullptr;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Loaders::setStoppedState(Mixer::Track *track, State state) {
|
||||
mixer()->setStoppedState(track, state);
|
||||
}
|
||||
|
||||
void Loaders::emitError(AudioMsgId::Type type) {
|
||||
error(clear(type));
|
||||
}
|
||||
|
||||
void Loaders::onLoad(const AudioMsgId &audio) {
|
||||
loadData(audio);
|
||||
}
|
||||
|
||||
void Loaders::loadData(AudioMsgId audio, crl::time positionMs) {
|
||||
auto type = audio.type();
|
||||
auto setup = setupLoader(audio, positionMs);
|
||||
const auto l = setup.loader;
|
||||
if (!l) {
|
||||
if (setup.errorAtStart) {
|
||||
emitError(type);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const auto sampleSize = l->sampleSize();
|
||||
const auto speedChanged = !EqualSpeeds(setup.newSpeed, setup.oldSpeed);
|
||||
auto updatedWithSpeed = speedChanged
|
||||
? rebufferOnSpeedChange(setup)
|
||||
: std::optional<Mixer::Track::WithSpeed>();
|
||||
if (!speedChanged && setup.oldSpeed > 0.) {
|
||||
const auto normalPosition = Mixer::Track::SpeedIndependentPosition(
|
||||
setup.position,
|
||||
setup.oldSpeed);
|
||||
l->dropFramesTill(normalPosition);
|
||||
}
|
||||
|
||||
const auto started = setup.justStarted;
|
||||
auto finished = false;
|
||||
auto waiting = false;
|
||||
auto errAtStart = started;
|
||||
|
||||
auto accumulated = QByteArray();
|
||||
auto accumulatedCount = 0;
|
||||
if (l->holdsSavedDecodedSamples()) {
|
||||
l->takeSavedDecodedSamples(&accumulated);
|
||||
accumulatedCount = accumulated.size() / sampleSize;
|
||||
}
|
||||
const auto accumulateTill = l->bytesPerBuffer();
|
||||
while (accumulated.size() < accumulateTill) {
|
||||
using Error = AudioPlayerLoader::ReadError;
|
||||
const auto result = l->readMore();
|
||||
if (result == Error::Retry) {
|
||||
continue;
|
||||
}
|
||||
const auto sampleBytes = v::is<bytes::const_span>(result)
|
||||
? v::get<bytes::const_span>(result)
|
||||
: bytes::const_span();
|
||||
if (!sampleBytes.empty()) {
|
||||
accumulated.append(
|
||||
reinterpret_cast<const char*>(sampleBytes.data()),
|
||||
sampleBytes.size());
|
||||
accumulatedCount += sampleBytes.size() / sampleSize;
|
||||
} else if (result == Error::Other) {
|
||||
if (errAtStart) {
|
||||
{
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
if (auto track = checkLoader(type)) {
|
||||
track->state.state = State::StoppedAtStart;
|
||||
}
|
||||
}
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
break;
|
||||
} else if (result == Error::EndOfFile) {
|
||||
finished = true;
|
||||
break;
|
||||
} else if (result == Error::Wait) {
|
||||
waiting = (accumulated.size() < accumulateTill)
|
||||
&& (accumulated.isEmpty() || !l->forceToBuffer());
|
||||
if (waiting) {
|
||||
l->saveDecodedSamples(&accumulated);
|
||||
}
|
||||
break;
|
||||
} else if (v::is<bytes::const_span>(result)) {
|
||||
errAtStart = false;
|
||||
}
|
||||
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
if (!checkLoader(type)) {
|
||||
clear(type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
auto track = checkLoader(type);
|
||||
if (!track) {
|
||||
clear(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (started || !accumulated.isEmpty() || updatedWithSpeed) {
|
||||
Audio::AttachToDevice();
|
||||
}
|
||||
if (started) {
|
||||
Assert(!updatedWithSpeed);
|
||||
track->started();
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtStart);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
track->format = l->format();
|
||||
track->state.frequency = l->samplesFrequency();
|
||||
|
||||
track->state.position = (positionMs * track->state.frequency)
|
||||
/ 1000LL;
|
||||
track->updateWithSpeedPosition();
|
||||
track->withSpeed.bufferedPosition = track->withSpeed.position;
|
||||
track->withSpeed.fadeStartPosition = track->withSpeed.position;
|
||||
} else if (updatedWithSpeed) {
|
||||
auto old = Mixer::Track();
|
||||
old.stream = base::take(track->stream);
|
||||
old.withSpeed = std::exchange(track->withSpeed, *updatedWithSpeed);
|
||||
track->speed = setup.newSpeed;
|
||||
track->reattach(type);
|
||||
old.detach();
|
||||
}
|
||||
if (!accumulated.isEmpty()) {
|
||||
track->ensureStreamCreated(type);
|
||||
|
||||
auto bufferIndex = track->getNotQueuedBufferIndex();
|
||||
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bufferIndex < 0) { // No free buffers, wait.
|
||||
track->waitingForBuffer = true;
|
||||
l->saveDecodedSamples(&accumulated);
|
||||
return;
|
||||
} else if (l->forceToBuffer()) {
|
||||
l->setForceToBuffer(false);
|
||||
}
|
||||
track->waitingForBuffer = false;
|
||||
|
||||
track->withSpeed.buffered[bufferIndex] = accumulated;
|
||||
track->withSpeed.samples[bufferIndex] = accumulatedCount;
|
||||
track->withSpeed.bufferedLength += accumulatedCount;
|
||||
alBufferData(
|
||||
track->stream.buffers[bufferIndex],
|
||||
track->format,
|
||||
accumulated.constData(),
|
||||
accumulated.size(),
|
||||
track->state.frequency);
|
||||
|
||||
alSourceQueueBuffers(
|
||||
track->stream.source,
|
||||
1,
|
||||
track->stream.buffers + bufferIndex);
|
||||
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (waiting) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
}
|
||||
track->state.waitingForData = false;
|
||||
|
||||
if (finished) {
|
||||
track->loaded = true;
|
||||
track->withSpeed.length = track->withSpeed.bufferedPosition
|
||||
+ track->withSpeed.bufferedLength;
|
||||
track->state.length = Mixer::Track::SpeedIndependentPosition(
|
||||
track->withSpeed.length,
|
||||
track->speed);
|
||||
}
|
||||
|
||||
track->loading = false;
|
||||
if (IsPausedOrPausing(track->state.state)
|
||||
|| IsStoppedOrStopping(track->state.state)) {
|
||||
return;
|
||||
}
|
||||
ALint state = AL_INITIAL;
|
||||
alGetSourcei(track->stream.source, AL_SOURCE_STATE, &state);
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == AL_PLAYING) {
|
||||
return;
|
||||
} else if (state == AL_STOPPED && !internal::CheckAudioDeviceConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
alSourcef(track->stream.source, AL_GAIN, ComputeVolume(type));
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == AL_STOPPED) {
|
||||
alSourcei(
|
||||
track->stream.source,
|
||||
AL_SAMPLE_OFFSET,
|
||||
qMax(track->withSpeed.position - track->withSpeed.bufferedPosition, 0LL));
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
}
|
||||
alSourcePlay(track->stream.source);
|
||||
if (!internal::audioCheckError()) {
|
||||
setStoppedState(track, State::StoppedAtError);
|
||||
emitError(type);
|
||||
return;
|
||||
}
|
||||
|
||||
needToCheck();
|
||||
}
|
||||
|
||||
Loaders::SetupLoaderResult Loaders::setupLoader(
|
||||
const AudioMsgId &audio,
|
||||
crl::time positionMs) {
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
if (!mixer()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto track = mixer()->trackForType(audio.type());
|
||||
if (!track || track->state.id != audio || !track->loading) {
|
||||
error(audio);
|
||||
LOG(("Audio Error: trying to load part of audio, that is not current at the moment"));
|
||||
return {};
|
||||
}
|
||||
|
||||
bool isGoodId = false;
|
||||
AudioPlayerLoader *l = nullptr;
|
||||
switch (audio.type()) {
|
||||
case AudioMsgId::Type::Voice: l = _audioLoader.get(); isGoodId = (_audio == audio); break;
|
||||
case AudioMsgId::Type::Song: l = _songLoader.get(); isGoodId = (_song == audio); break;
|
||||
case AudioMsgId::Type::Video: l = _videoLoader.get(); isGoodId = (_video == audio); break;
|
||||
}
|
||||
|
||||
if (l && (!isGoodId || !l->check(track->file, track->data))) {
|
||||
clear(audio.type());
|
||||
l = nullptr;
|
||||
}
|
||||
|
||||
auto SpeedDependentPosition = Mixer::Track::SpeedDependentPosition;
|
||||
if (!l) {
|
||||
std::unique_ptr<AudioPlayerLoader> *loader = nullptr;
|
||||
switch (audio.type()) {
|
||||
case AudioMsgId::Type::Voice: _audio = audio; loader = &_audioLoader; break;
|
||||
case AudioMsgId::Type::Song: _song = audio; loader = &_songLoader; break;
|
||||
case AudioMsgId::Type::Video: _video = audio; loader = &_videoLoader; break;
|
||||
}
|
||||
|
||||
if (audio.externalPlayId()) {
|
||||
if (!track->externalData) {
|
||||
clear(audio.type());
|
||||
track->state.state = State::StoppedAtError;
|
||||
error(audio);
|
||||
LOG(("Audio Error: video sound data not ready"));
|
||||
return {};
|
||||
}
|
||||
*loader = std::make_unique<ChildFFMpegLoader>(
|
||||
std::move(track->externalData));
|
||||
} else {
|
||||
*loader = std::make_unique<FFMpegLoader>(
|
||||
track->file,
|
||||
track->data,
|
||||
bytes::vector());
|
||||
}
|
||||
l = loader->get();
|
||||
|
||||
track->speed = track->nextSpeed;
|
||||
if (!l->open(positionMs, track->speed)) {
|
||||
track->state.state = State::StoppedAtStart;
|
||||
return { .errorAtStart = true };
|
||||
}
|
||||
const auto duration = l->duration();
|
||||
if (duration <= 0) {
|
||||
track->state.state = State::StoppedAtStart;
|
||||
return { .errorAtStart = true };
|
||||
}
|
||||
track->state.frequency = l->samplesFrequency();
|
||||
track->state.length = (duration * track->state.frequency) / 1000;
|
||||
track->withSpeed.length = SpeedDependentPosition(
|
||||
track->state.length,
|
||||
track->speed);
|
||||
return { .loader = l, .justStarted = true };
|
||||
} else if (!EqualSpeeds(track->nextSpeed, track->speed)) {
|
||||
return {
|
||||
.loader = l,
|
||||
.oldSpeed = track->speed,
|
||||
.newSpeed = track->nextSpeed,
|
||||
.fadeStartPosition = track->withSpeed.fadeStartPosition,
|
||||
.position = track->withSpeed.fineTunedPosition,
|
||||
.normalLength = track->state.length,
|
||||
.frequency = track->state.frequency,
|
||||
};
|
||||
} else if (track->loaded) {
|
||||
LOG(("Audio Error: trying to load part of audio, that is already loaded to the end"));
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
.loader = l,
|
||||
.oldSpeed = track->speed,
|
||||
.newSpeed = track->nextSpeed,
|
||||
.position = track->withSpeed.fineTunedPosition,
|
||||
.frequency = track->state.frequency,
|
||||
};
|
||||
}
|
||||
|
||||
Mixer::Track::WithSpeed Loaders::rebufferOnSpeedChange(
|
||||
const SetupLoaderResult &setup) {
|
||||
Expects(setup.oldSpeed > 0. && setup.newSpeed > 0.);
|
||||
Expects(setup.loader != nullptr);
|
||||
|
||||
const auto speed = setup.newSpeed;
|
||||
const auto change = setup.oldSpeed / speed;
|
||||
const auto normalPosition = Mixer::Track::SpeedIndependentPosition(
|
||||
setup.position,
|
||||
setup.oldSpeed);
|
||||
const auto newPosition = int64(base::SafeRound(setup.position * change));
|
||||
auto result = Mixer::Track::WithSpeed{
|
||||
.fineTunedPosition = newPosition,
|
||||
.position = newPosition,
|
||||
.length = Mixer::Track::SpeedDependentPosition(
|
||||
setup.normalLength,
|
||||
speed),
|
||||
.fadeStartPosition = int64(
|
||||
base::SafeRound(setup.fadeStartPosition * change)),
|
||||
};
|
||||
const auto l = setup.loader;
|
||||
l->dropFramesTill(normalPosition);
|
||||
const auto normalFrom = l->startReadingQueuedFrames(speed);
|
||||
if (normalFrom < 0) {
|
||||
result.bufferedPosition = newPosition;
|
||||
return result;
|
||||
}
|
||||
|
||||
result.bufferedPosition = Mixer::Track::SpeedDependentPosition(
|
||||
normalFrom,
|
||||
speed);
|
||||
for (auto i = 0; i != Mixer::Track::kBuffersCount; ++i) {
|
||||
auto finished = false;
|
||||
auto accumulated = QByteArray();
|
||||
auto accumulatedCount = int64();
|
||||
const auto sampleSize = l->sampleSize();
|
||||
const auto accumulateTill = l->bytesPerBuffer();
|
||||
while (accumulated.size() < accumulateTill) {
|
||||
const auto result = l->readMore();
|
||||
const auto sampleBytes = v::is<bytes::const_span>(result)
|
||||
? v::get<bytes::const_span>(result)
|
||||
: bytes::const_span();
|
||||
if (!sampleBytes.empty()) {
|
||||
accumulated.append(
|
||||
reinterpret_cast<const char*>(sampleBytes.data()),
|
||||
sampleBytes.size());
|
||||
accumulatedCount += sampleBytes.size() / sampleSize;
|
||||
continue;
|
||||
} else if (result == AudioPlayerLoader::ReadError::Retry) {
|
||||
continue;
|
||||
}
|
||||
Assert(result == AudioPlayerLoader::ReadError::RetryNotQueued
|
||||
|| result == AudioPlayerLoader::ReadError::EndOfFile);
|
||||
finished = true;
|
||||
break;
|
||||
}
|
||||
if (!accumulated.isEmpty()) {
|
||||
result.samples[i] = accumulatedCount;
|
||||
result.bufferedLength += accumulatedCount;
|
||||
result.buffered[i] = accumulated;
|
||||
}
|
||||
if (finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const auto limit = result.bufferedPosition + result.bufferedLength;
|
||||
if (newPosition > limit) {
|
||||
result.fineTunedPosition = limit;
|
||||
result.position = limit;
|
||||
}
|
||||
if (limit > result.length) {
|
||||
result.length = limit;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Mixer::Track *Loaders::checkLoader(AudioMsgId::Type type) {
|
||||
if (!mixer()) return nullptr;
|
||||
|
||||
auto track = mixer()->trackForType(type);
|
||||
auto isGoodId = false;
|
||||
AudioPlayerLoader *l = nullptr;
|
||||
switch (type) {
|
||||
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 || !track) return nullptr;
|
||||
|
||||
if (!isGoodId || !track->loading || !l->check(track->file, track->data)) {
|
||||
LOG(("Audio Error: playing changed while loading"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
void Loaders::onCancel(const AudioMsgId &audio) {
|
||||
Expects(audio.type() != AudioMsgId::Type::Unknown);
|
||||
|
||||
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;
|
||||
case AudioMsgId::Type::Video: if (_video == audio) clear(audio.type()); break;
|
||||
}
|
||||
|
||||
QMutexLocker lock(internal::audioPlayerMutex());
|
||||
if (!mixer()) return;
|
||||
|
||||
for (auto i = 0; i != kTogetherLimit; ++i) {
|
||||
auto track = mixer()->trackForType(audio.type(), i);
|
||||
if (track->state.id == audio) {
|
||||
track->loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loaders::~Loaders() = default;
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
81
Telegram/SourceFiles/media/audio/media_audio_loaders.h
Normal file
81
Telegram/SourceFiles/media/audio/media_audio_loaders.h
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "media/audio/media_child_ffmpeg_loader.h"
|
||||
|
||||
class AudioPlayerLoader;
|
||||
class ChildFFMpegLoader;
|
||||
|
||||
namespace Media {
|
||||
namespace Player {
|
||||
|
||||
class Loaders : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Loaders(QThread *thread);
|
||||
void feedFromExternal(ExternalSoundPart &&part);
|
||||
void forceToBufferExternal(const AudioMsgId &audioId);
|
||||
~Loaders();
|
||||
|
||||
Q_SIGNALS:
|
||||
void error(const AudioMsgId &audio);
|
||||
void needToCheck();
|
||||
|
||||
public Q_SLOTS:
|
||||
void onInit();
|
||||
|
||||
void onStart(const AudioMsgId &audio, qint64 positionMs);
|
||||
void onLoad(const AudioMsgId &audio);
|
||||
void onCancel(const AudioMsgId &audio);
|
||||
|
||||
private:
|
||||
struct SetupLoaderResult {
|
||||
AudioPlayerLoader *loader = nullptr;
|
||||
float64 oldSpeed = 0.;
|
||||
float64 newSpeed = 0.;
|
||||
int64 fadeStartPosition = 0;
|
||||
int64 position = 0;
|
||||
int64 normalLength = 0;
|
||||
int frequency = 0;
|
||||
bool errorAtStart = false;
|
||||
bool justStarted = false;
|
||||
};
|
||||
|
||||
void videoSoundAdded();
|
||||
[[nodiscard]] Mixer::Track::WithSpeed rebufferOnSpeedChange(
|
||||
const SetupLoaderResult &setup);
|
||||
|
||||
void emitError(AudioMsgId::Type type);
|
||||
AudioMsgId clear(AudioMsgId::Type type);
|
||||
void setStoppedState(Mixer::Track *m, State state = State::Stopped);
|
||||
|
||||
void loadData(AudioMsgId audio, crl::time positionMs = 0);
|
||||
[[nodiscard]] SetupLoaderResult setupLoader(
|
||||
const AudioMsgId &audio,
|
||||
crl::time positionMs);
|
||||
Mixer::Track *checkLoader(AudioMsgId::Type type);
|
||||
|
||||
AudioMsgId _audio, _song, _video;
|
||||
std::unique_ptr<AudioPlayerLoader> _audioLoader;
|
||||
std::unique_ptr<AudioPlayerLoader> _songLoader;
|
||||
std::unique_ptr<AudioPlayerLoader> _videoLoader;
|
||||
|
||||
QMutex _fromExternalMutex;
|
||||
base::flat_map<
|
||||
AudioMsgId,
|
||||
std::deque<FFmpeg::Packet>> _fromExternalQueues;
|
||||
base::flat_set<AudioMsgId> _fromExternalForceToBuffer;
|
||||
SingleQueuedInvokation _fromExternalNotify;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Player
|
||||
} // namespace Media
|
||||
339
Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp
Normal file
339
Telegram/SourceFiles/media/audio/media_audio_local_cache.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_local_cache.h"
|
||||
|
||||
#include "ffmpeg/ffmpeg_bytes_io_wrap.h"
|
||||
#include "ffmpeg/ffmpeg_utility.h"
|
||||
|
||||
namespace Media::Audio {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxDuration = 3 * crl::time(1000);
|
||||
constexpr auto kMaxStreams = 2;
|
||||
constexpr auto kFrameSize = 4096;
|
||||
|
||||
[[nodiscard]] QByteArray ConvertAndCut(const QByteArray &bytes) {
|
||||
using namespace FFmpeg;
|
||||
|
||||
if (bytes.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto wrap = ReadBytesWrap{
|
||||
.size = bytes.size(),
|
||||
.data = reinterpret_cast<const uchar*>(bytes.constData()),
|
||||
};
|
||||
|
||||
auto input = MakeFormatPointer(
|
||||
&wrap,
|
||||
&ReadBytesWrap::Read,
|
||||
nullptr,
|
||||
&ReadBytesWrap::Seek);
|
||||
if (!input) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto error = AvErrorWrap(avformat_find_stream_info(input.get(), 0));
|
||||
if (error) {
|
||||
LogError(u"avformat_find_stream_info"_q, error);
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
auto inCodec = (const AVCodec*)nullptr;
|
||||
const auto streamId = av_find_best_stream(
|
||||
input.get(),
|
||||
AVMEDIA_TYPE_AUDIO,
|
||||
-1,
|
||||
-1,
|
||||
&inCodec,
|
||||
0);
|
||||
if (streamId < 0) {
|
||||
LogError(u"av_find_best_stream"_q, AvErrorWrap(streamId));
|
||||
return {};
|
||||
}
|
||||
|
||||
auto inStream = input->streams[streamId];
|
||||
auto inCodecPar = inStream->codecpar;
|
||||
auto inCodecContext = CodecPointer(avcodec_alloc_context3(nullptr));
|
||||
if (!inCodecContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (avcodec_parameters_to_context(inCodecContext.get(), inCodecPar) < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (avcodec_open2(inCodecContext.get(), inCodec, nullptr) < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto result = WriteBytesWrap();
|
||||
auto outFormat = MakeWriteFormatPointer(
|
||||
static_cast<void*>(&result),
|
||||
nullptr,
|
||||
&WriteBytesWrap::Write,
|
||||
&WriteBytesWrap::Seek,
|
||||
"wav"_q);
|
||||
if (!outFormat) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Find and open output codec
|
||||
auto outCodec = avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);
|
||||
if (!outCodec) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto outStream = avformat_new_stream(outFormat.get(), outCodec);
|
||||
if (!outStream) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto outCodecContext = CodecPointer(
|
||||
avcodec_alloc_context3(outCodec));
|
||||
if (!outCodecContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto mono = AVChannelLayout(AV_CHANNEL_LAYOUT_MONO);
|
||||
auto stereo = AVChannelLayout(AV_CHANNEL_LAYOUT_STEREO);
|
||||
const auto in = &inCodecContext->ch_layout;
|
||||
if (!av_channel_layout_compare(in, &mono)
|
||||
|| !av_channel_layout_compare(in, &stereo)) {
|
||||
av_channel_layout_copy(&outCodecContext->ch_layout, in);
|
||||
} else {
|
||||
outCodecContext->ch_layout = AV_CHANNEL_LAYOUT_STEREO;
|
||||
}
|
||||
const auto rate = 44'100;
|
||||
outCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
|
||||
outCodecContext->time_base = AVRational{ 1, rate };
|
||||
outCodecContext->sample_rate = rate;
|
||||
|
||||
error = avcodec_open2(outCodecContext.get(), outCodec, nullptr);
|
||||
if (error) {
|
||||
LogError("avcodec_open2", error);
|
||||
return {};
|
||||
}
|
||||
|
||||
error = avcodec_parameters_from_context(
|
||||
outStream->codecpar,
|
||||
outCodecContext.get());
|
||||
if (error) {
|
||||
LogError("avcodec_parameters_from_context", error);
|
||||
return {};
|
||||
}
|
||||
|
||||
error = avformat_write_header(outFormat.get(), nullptr);
|
||||
if (error) {
|
||||
LogError("avformat_write_header", error);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto swrContext = MakeSwresamplePointer(
|
||||
&inCodecContext->ch_layout,
|
||||
inCodecContext->sample_fmt,
|
||||
inCodecContext->sample_rate,
|
||||
&outCodecContext->ch_layout,
|
||||
outCodecContext->sample_fmt,
|
||||
outCodecContext->sample_rate);
|
||||
if (!swrContext) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto packet = av_packet_alloc();
|
||||
const auto guard = gsl::finally([&] {
|
||||
av_packet_free(&packet);
|
||||
});
|
||||
|
||||
auto frame = MakeFramePointer();
|
||||
if (!frame) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto outFrame = MakeFramePointer();
|
||||
if (!outFrame) {
|
||||
return {};
|
||||
}
|
||||
|
||||
outFrame->nb_samples = kFrameSize;
|
||||
outFrame->format = outCodecContext->sample_fmt;
|
||||
av_channel_layout_copy(
|
||||
&outFrame->ch_layout,
|
||||
&outCodecContext->ch_layout);
|
||||
outFrame->sample_rate = outCodecContext->sample_rate;
|
||||
|
||||
error = av_frame_get_buffer(outFrame.get(), 0);
|
||||
if (error) {
|
||||
LogError("av_frame_get_buffer", error);
|
||||
return {};
|
||||
}
|
||||
|
||||
auto pts = int64_t(0);
|
||||
auto maxPts = int64_t(kMaxDuration) * rate / 1000;
|
||||
const auto writeFrame = [&](AVFrame *frame) { // nullptr to flush
|
||||
error = avcodec_send_frame(outCodecContext.get(), frame);
|
||||
if (error) {
|
||||
LogError("avcodec_send_frame", error);
|
||||
return error;
|
||||
}
|
||||
auto pkt = av_packet_alloc();
|
||||
const auto guard = gsl::finally([&] {
|
||||
av_packet_free(&pkt);
|
||||
});
|
||||
while (true) {
|
||||
error = avcodec_receive_packet(outCodecContext.get(), pkt);
|
||||
if (error) {
|
||||
if (error.code() != AVERROR(EAGAIN)
|
||||
&& error.code() != AVERROR_EOF) {
|
||||
LogError("avcodec_receive_packet", error);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
pkt->stream_index = outStream->index;
|
||||
av_packet_rescale_ts(
|
||||
pkt,
|
||||
outCodecContext->time_base,
|
||||
outStream->time_base);
|
||||
error = av_interleaved_write_frame(outFormat.get(), pkt);
|
||||
if (error) {
|
||||
LogError("av_interleaved_write_frame", error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (pts < maxPts) {
|
||||
error = av_read_frame(input.get(), packet);
|
||||
const auto finished = (error.code() == AVERROR_EOF);
|
||||
if (!finished) {
|
||||
if (error) {
|
||||
LogError("av_read_frame", error);
|
||||
return {};
|
||||
}
|
||||
auto guard = gsl::finally([&] {
|
||||
av_packet_unref(packet);
|
||||
});
|
||||
if (packet->stream_index != streamId) {
|
||||
continue;
|
||||
}
|
||||
error = avcodec_send_packet(inCodecContext.get(), packet);
|
||||
if (error) {
|
||||
LogError("avcodec_send_packet", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
while (true) {
|
||||
error = avcodec_receive_frame(inCodecContext.get(), frame.get());
|
||||
if (error) {
|
||||
if (error.code() == AVERROR(EAGAIN)
|
||||
|| error.code() == AVERROR_EOF) {
|
||||
break;
|
||||
} else {
|
||||
LogError("avcodec_receive_frame", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
error = swr_convert(
|
||||
swrContext.get(),
|
||||
outFrame->data,
|
||||
kFrameSize,
|
||||
(const uint8_t**)frame->data,
|
||||
frame->nb_samples);
|
||||
if (error) {
|
||||
LogError("swr_convert", error);
|
||||
return {};
|
||||
}
|
||||
const auto samples = error.code();
|
||||
if (!samples) {
|
||||
continue;
|
||||
}
|
||||
|
||||
outFrame->nb_samples = samples;
|
||||
outFrame->pts = pts;
|
||||
pts += samples;
|
||||
if (pts > maxPts) {
|
||||
break;
|
||||
}
|
||||
|
||||
error = writeFrame(outFrame.get());
|
||||
if (error && error.code() != AVERROR(EAGAIN)) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
error = writeFrame(nullptr);
|
||||
if (error && error.code() != AVERROR_EOF) {
|
||||
return {};
|
||||
}
|
||||
error = av_write_trailer(outFormat.get());
|
||||
if (error) {
|
||||
LogError("av_write_trailer", error);
|
||||
return {};
|
||||
}
|
||||
return result.content;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
LocalSound LocalCache::sound(
|
||||
DocumentId id,
|
||||
Fn<QByteArray()> resolveOriginalBytes,
|
||||
Fn<QByteArray()> fallbackOriginalBytes) {
|
||||
auto &result = _cache[id];
|
||||
if (!result.isEmpty()) {
|
||||
return { id, result };
|
||||
}
|
||||
result = ConvertAndCut(resolveOriginalBytes());
|
||||
return !result.isEmpty()
|
||||
? LocalSound{ id, result }
|
||||
: fallbackOriginalBytes
|
||||
? sound(0, fallbackOriginalBytes, nullptr)
|
||||
: LocalSound();
|
||||
}
|
||||
|
||||
LocalDiskCache::LocalDiskCache(const QString &folder)
|
||||
: _base(folder + '/') {
|
||||
QDir().mkpath(_base);
|
||||
}
|
||||
|
||||
QString LocalDiskCache::name(const LocalSound &sound) {
|
||||
if (!sound) {
|
||||
return {};
|
||||
}
|
||||
const auto i = _paths.find(sound.id);
|
||||
if (i != end(_paths)) {
|
||||
return i->second;
|
||||
}
|
||||
|
||||
auto result = u"TD_%1"_q.arg(sound.id
|
||||
? QString::number(sound.id, 16).toUpper()
|
||||
: u"Default"_q);
|
||||
const auto path = _base + u"%1.wav"_q.arg(result);
|
||||
|
||||
auto f = QFile(path);
|
||||
if (f.open(QIODevice::WriteOnly)) {
|
||||
f.write(sound.wav);
|
||||
f.close();
|
||||
}
|
||||
|
||||
_paths.emplace(sound.id, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
QString LocalDiskCache::path(const LocalSound &sound) {
|
||||
const auto part = name(sound);
|
||||
return part.isEmpty() ? QString() : _base + part + u".wav"_q;
|
||||
}
|
||||
|
||||
} // namespace Media::Audio
|
||||
46
Telegram/SourceFiles/media/audio/media_audio_local_cache.h
Normal file
46
Telegram/SourceFiles/media/audio/media_audio_local_cache.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace Media::Audio {
|
||||
|
||||
struct LocalSound {
|
||||
DocumentId id = 0;
|
||||
QByteArray wav;
|
||||
|
||||
explicit operator bool() const {
|
||||
return !wav.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
class LocalCache final {
|
||||
public:
|
||||
[[nodiscard]] LocalSound sound(
|
||||
DocumentId id,
|
||||
Fn<QByteArray()> resolveOriginalBytes,
|
||||
Fn<QByteArray()> fallbackOriginalBytes);
|
||||
|
||||
private:
|
||||
base::flat_map<DocumentId, QByteArray> _cache;
|
||||
|
||||
};
|
||||
|
||||
class LocalDiskCache final {
|
||||
public:
|
||||
explicit LocalDiskCache(const QString &folder);
|
||||
|
||||
[[nodiscard]] QString name(const LocalSound &sound);
|
||||
[[nodiscard]] QString path(const LocalSound &sound);
|
||||
|
||||
private:
|
||||
const QString _base;
|
||||
base::flat_map<DocumentId, QString> _paths;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media::Audio
|
||||
372
Telegram/SourceFiles/media/audio/media_audio_track.cpp
Normal file
372
Telegram/SourceFiles/media/audio/media_audio_track.cpp
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_audio_track.h"
|
||||
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
#include "media/audio/media_audio.h"
|
||||
#include "core/application.h"
|
||||
#include "core/core_settings.h"
|
||||
#include "core/file_location.h"
|
||||
|
||||
#include <al.h>
|
||||
#include <alc.h>
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
namespace {
|
||||
|
||||
constexpr auto kMaxFileSize = 10 * 1024 * 1024;
|
||||
constexpr auto kDetachDeviceTimeout = crl::time(500); // destroy the audio device after 500ms of silence
|
||||
constexpr auto kTrackUpdateTimeout = crl::time(100);
|
||||
|
||||
ALuint CreateSource() {
|
||||
auto source = ALuint(0);
|
||||
alGenSources(1, &source);
|
||||
alSourcef(source, AL_PITCH, 1.f);
|
||||
alSourcef(source, AL_GAIN, 1.f);
|
||||
alSource3f(source, AL_POSITION, 0, 0, 0);
|
||||
alSource3f(source, AL_VELOCITY, 0, 0, 0);
|
||||
return source;
|
||||
}
|
||||
|
||||
ALuint CreateBuffer() {
|
||||
auto buffer = ALuint(0);
|
||||
alGenBuffers(1, &buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Track::Track(not_null<Instance*> instance) : _instance(instance) {
|
||||
_instance->registerTrack(this);
|
||||
}
|
||||
|
||||
void Track::samplePeakEach(crl::time peakDuration) {
|
||||
_peakDurationMs = peakDuration;
|
||||
}
|
||||
|
||||
void Track::fillFromData(bytes::vector &&data) {
|
||||
FFMpegLoader loader(Core::FileLocation(), QByteArray(), std::move(data));
|
||||
|
||||
auto position = qint64(0);
|
||||
if (!loader.open(position)) {
|
||||
_failed = true;
|
||||
return;
|
||||
}
|
||||
auto format = loader.format();
|
||||
_peakEachPosition = _peakDurationMs ? ((loader.samplesFrequency() * _peakDurationMs) / 1000) : 0;
|
||||
const auto samplesCount = (loader.duration() * loader.samplesFrequency()) / 1000;
|
||||
const auto peaksCount = _peakEachPosition ? (samplesCount / _peakEachPosition) : 0;
|
||||
_peaks.reserve(peaksCount);
|
||||
auto peakValue = uint16(0);
|
||||
auto peakSamples = 0;
|
||||
auto peakEachSample = (format == AL_FORMAT_STEREO8 || format == AL_FORMAT_STEREO16) ? (_peakEachPosition * 2) : _peakEachPosition;
|
||||
_peakValueMin = 0x7FFF;
|
||||
_peakValueMax = 0;
|
||||
auto peakCallback = [this, &peakValue, &peakSamples, peakEachSample](uint16 sample) {
|
||||
accumulate_max(peakValue, sample);
|
||||
if (++peakSamples >= peakEachSample) {
|
||||
peakSamples -= peakEachSample;
|
||||
_peaks.push_back(peakValue);
|
||||
accumulate_max(_peakValueMax, peakValue);
|
||||
accumulate_min(_peakValueMin, peakValue);
|
||||
peakValue = 0;
|
||||
}
|
||||
};
|
||||
do {
|
||||
using Error = AudioPlayerLoader::ReadError;
|
||||
const auto result = loader.readMore();
|
||||
Assert(result != Error::Wait && result != Error::RetryNotQueued);
|
||||
|
||||
if (result == Error::Retry) {
|
||||
continue;
|
||||
} else if (result == Error::EndOfFile) {
|
||||
break;
|
||||
} else if (result == Error::Other || result == Error::Wait) {
|
||||
_failed = true;
|
||||
break;
|
||||
}
|
||||
Assert(v::is<bytes::const_span>(result));
|
||||
const auto sampleBytes = v::get<bytes::const_span>(result);
|
||||
Assert(!sampleBytes.empty());
|
||||
_samplesCount += sampleBytes.size() / loader.sampleSize();
|
||||
_samples.insert(_samples.end(), sampleBytes.data(), sampleBytes.data() + sampleBytes.size());
|
||||
if (peaksCount) {
|
||||
if (format == AL_FORMAT_MONO8 || format == AL_FORMAT_STEREO8) {
|
||||
Media::Audio::IterateSamples<uchar>(sampleBytes, peakCallback);
|
||||
} else if (format == AL_FORMAT_MONO16 || format == AL_FORMAT_STEREO16) {
|
||||
Media::Audio::IterateSamples<int16>(sampleBytes, peakCallback);
|
||||
}
|
||||
}
|
||||
} while (true);
|
||||
|
||||
_alFormat = loader.format();
|
||||
_sampleRate = loader.samplesFrequency();
|
||||
_lengthMs = loader.duration();
|
||||
}
|
||||
|
||||
void Track::fillFromFile(const Core::FileLocation &location) {
|
||||
if (location.accessEnable()) {
|
||||
fillFromFile(location.name());
|
||||
location.accessDisable();
|
||||
} else {
|
||||
LOG(("Track Error: Could not enable access to file '%1'.").arg(location.name()));
|
||||
_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Track::fillFromFile(const QString &filePath) {
|
||||
QFile f(filePath);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
auto size = f.size();
|
||||
if (size > 0 && size <= kMaxFileSize) {
|
||||
auto bytes = bytes::vector(size);
|
||||
if (f.read(reinterpret_cast<char*>(bytes.data()), bytes.size()) == bytes.size()) {
|
||||
fillFromData(std::move(bytes));
|
||||
} else {
|
||||
LOG(("Track Error: Could not read %1 bytes from file '%2'.").arg(bytes.size()).arg(filePath));
|
||||
_failed = true;
|
||||
}
|
||||
} else {
|
||||
LOG(("Track Error: Bad file '%1' size: %2.").arg(filePath).arg(size));
|
||||
_failed = true;
|
||||
}
|
||||
} else {
|
||||
LOG(("Track Error: Could not open file '%1'.").arg(filePath));
|
||||
_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Track::playWithLooping(bool looping, float64 volumeOverride) {
|
||||
_active = true;
|
||||
if (failed() || _samples.empty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
ensureSourceCreated();
|
||||
alSourceStop(_alSource);
|
||||
_looping = looping;
|
||||
alSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0);
|
||||
alSourcef(
|
||||
_alSource,
|
||||
AL_GAIN,
|
||||
(volumeOverride > 0)
|
||||
? volumeOverride
|
||||
: float64(Core::App().settings().notificationsVolume()) / 100.);
|
||||
alSourcePlay(_alSource);
|
||||
_instance->trackStarted(this);
|
||||
}
|
||||
|
||||
void Track::finish() {
|
||||
if (_active) {
|
||||
_active = false;
|
||||
_instance->trackFinished(this);
|
||||
}
|
||||
_alPosition = 0;
|
||||
}
|
||||
|
||||
void Track::ensureSourceCreated() {
|
||||
if (alIsSource(_alSource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
QMutexLocker lock(Player::internal::audioPlayerMutex());
|
||||
if (!AttachToDevice()) {
|
||||
_failed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_alSource = CreateSource();
|
||||
_alBuffer = CreateBuffer();
|
||||
|
||||
alBufferData(_alBuffer, _alFormat, _samples.data(), _samples.size(), _sampleRate);
|
||||
alSourcei(_alSource, AL_BUFFER, _alBuffer);
|
||||
}
|
||||
|
||||
void Track::updateState() {
|
||||
if (!isActive() || !alIsSource(_alSource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_stateUpdatedAt = crl::now();
|
||||
auto state = ALint(0);
|
||||
alGetSourcei(_alSource, AL_SOURCE_STATE, &state);
|
||||
if (state != AL_PLAYING) {
|
||||
finish();
|
||||
} else {
|
||||
auto currentPosition = ALint(0);
|
||||
alGetSourcei(_alSource, AL_SAMPLE_OFFSET, ¤tPosition);
|
||||
_alPosition = currentPosition;
|
||||
}
|
||||
}
|
||||
|
||||
float64 Track::getPeakValue(crl::time when) const {
|
||||
if (!isActive() || !_samplesCount || _peaks.empty() || _peakValueMin == _peakValueMax) {
|
||||
return 0.;
|
||||
}
|
||||
auto sampleIndex = (_alPosition + ((when - _stateUpdatedAt) * _sampleRate / 1000));
|
||||
while (sampleIndex < 0) {
|
||||
sampleIndex += _samplesCount;
|
||||
}
|
||||
sampleIndex = sampleIndex % _samplesCount;
|
||||
auto peakIndex = (sampleIndex / _peakEachPosition) % _peaks.size();
|
||||
return (_peaks[peakIndex] - _peakValueMin) / float64(_peakValueMax - _peakValueMin);
|
||||
}
|
||||
|
||||
void Track::detachFromDevice() {
|
||||
if (alIsSource(_alSource)) {
|
||||
updateState();
|
||||
alSourceStop(_alSource);
|
||||
alSourcei(_alSource, AL_BUFFER, AL_NONE);
|
||||
alDeleteBuffers(1, &_alBuffer);
|
||||
alDeleteSources(1, &_alSource);
|
||||
}
|
||||
_alBuffer = 0;
|
||||
_alSource = 0;
|
||||
}
|
||||
|
||||
void Track::reattachToDevice() {
|
||||
if (!isActive() || alIsSource(_alSource)) {
|
||||
return;
|
||||
}
|
||||
ensureSourceCreated();
|
||||
|
||||
alSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0);
|
||||
alSourcei(_alSource, AL_SAMPLE_OFFSET, static_cast<ALint>(_alPosition));
|
||||
alSourcePlay(_alSource);
|
||||
}
|
||||
|
||||
Track::~Track() {
|
||||
detachFromDevice();
|
||||
_instance->unregisterTrack(this);
|
||||
}
|
||||
|
||||
Instance::Instance()
|
||||
: _playbackDeviceId(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Playback,
|
||||
Webrtc::DeviceIdOrDefault(
|
||||
Core::App().settings().playbackDeviceIdValue()))
|
||||
, _captureDeviceId(
|
||||
&Core::App().mediaDevices(),
|
||||
Webrtc::DeviceType::Capture,
|
||||
Webrtc::DeviceIdOrDefault(
|
||||
Core::App().settings().captureDeviceIdValue())) {
|
||||
_updateTimer.setCallback([this] {
|
||||
auto hasActive = false;
|
||||
for (auto track : _tracks) {
|
||||
track->updateState();
|
||||
if (track->isActive()) {
|
||||
hasActive = true;
|
||||
}
|
||||
}
|
||||
if (hasActive) {
|
||||
Audio::StopDetachIfNotUsedSafe();
|
||||
}
|
||||
});
|
||||
|
||||
_detachFromDeviceTimer.setCallback([=] {
|
||||
_detachFromDeviceForce = false;
|
||||
Player::internal::DetachFromDevice(this);
|
||||
});
|
||||
|
||||
_playbackDeviceId.changes(
|
||||
) | rpl::on_next([=](Webrtc::DeviceResolvedId id) {
|
||||
if (Player::internal::DetachIfDeviceChanged(this, id)) {
|
||||
_detachFromDeviceForce = false;
|
||||
}
|
||||
}, _lifetime);
|
||||
}
|
||||
|
||||
Webrtc::DeviceResolvedId Instance::playbackDeviceId() const {
|
||||
return _playbackDeviceId.threadSafeCurrent();
|
||||
}
|
||||
|
||||
Webrtc::DeviceResolvedId Instance::captureDeviceId() const {
|
||||
return _captureDeviceId.current();
|
||||
}
|
||||
|
||||
std::unique_ptr<Track> Instance::createTrack() {
|
||||
return std::make_unique<Track>(this);
|
||||
}
|
||||
|
||||
Instance::~Instance() {
|
||||
Expects(_tracks.empty());
|
||||
}
|
||||
|
||||
void Instance::registerTrack(Track *track) {
|
||||
_tracks.insert(track);
|
||||
}
|
||||
|
||||
void Instance::unregisterTrack(Track *track) {
|
||||
_tracks.erase(track);
|
||||
}
|
||||
|
||||
void Instance::trackStarted(Track *track) {
|
||||
stopDetachIfNotUsed();
|
||||
if (!_updateTimer.isActive()) {
|
||||
_updateTimer.callEach(kTrackUpdateTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::trackFinished(Track *track) {
|
||||
if (!hasActiveTracks()) {
|
||||
_updateTimer.cancel();
|
||||
scheduleDetachIfNotUsed();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::detachTracks() {
|
||||
for (auto track : _tracks) {
|
||||
track->detachFromDevice();
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::reattachTracks() {
|
||||
if (!IsAttachedToDevice()) {
|
||||
return;
|
||||
}
|
||||
for (auto track : _tracks) {
|
||||
track->reattachToDevice();
|
||||
}
|
||||
}
|
||||
|
||||
bool Instance::hasActiveTracks() const {
|
||||
for (auto track : _tracks) {
|
||||
if (track->isActive()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Instance::scheduleDetachFromDevice() {
|
||||
_detachFromDeviceForce = true;
|
||||
scheduleDetachIfNotUsed();
|
||||
}
|
||||
|
||||
void Instance::scheduleDetachIfNotUsed() {
|
||||
if (!_detachFromDeviceTimer.isActive()) {
|
||||
_detachFromDeviceTimer.callOnce(kDetachDeviceTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
void Instance::stopDetachIfNotUsed() {
|
||||
if (!_detachFromDeviceForce) {
|
||||
_detachFromDeviceTimer.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
Instance &Current() {
|
||||
return Core::App().audio();
|
||||
}
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Media
|
||||
140
Telegram/SourceFiles/media/audio/media_audio_track.h
Normal file
140
Telegram/SourceFiles/media/audio/media_audio_track.h
Normal file
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "base/timer.h"
|
||||
#include "base/bytes.h"
|
||||
#include "webrtc/webrtc_device_resolver.h"
|
||||
|
||||
namespace Core {
|
||||
class FileLocation;
|
||||
} // namespace Core
|
||||
|
||||
namespace Media {
|
||||
namespace Audio {
|
||||
|
||||
class Instance;
|
||||
|
||||
class Track {
|
||||
public:
|
||||
Track(not_null<Instance*> instance);
|
||||
|
||||
void samplePeakEach(crl::time peakDuration);
|
||||
|
||||
void fillFromData(bytes::vector &&data);
|
||||
void fillFromFile(const Core::FileLocation &location);
|
||||
void fillFromFile(const QString &filePath);
|
||||
|
||||
void playOnce(float64 volumeOverride = -1) {
|
||||
playWithLooping(false, volumeOverride);
|
||||
}
|
||||
void playInLoop(float64 volumeOverride = -1) {
|
||||
playWithLooping(true, volumeOverride);
|
||||
}
|
||||
|
||||
bool isLooping() const {
|
||||
return _looping;
|
||||
}
|
||||
bool isActive() const {
|
||||
return _active;
|
||||
}
|
||||
bool failed() const {
|
||||
return _failed;
|
||||
}
|
||||
|
||||
int64 getLengthMs() const {
|
||||
return _lengthMs;
|
||||
}
|
||||
float64 getPeakValue(crl::time when) const;
|
||||
|
||||
void detachFromDevice();
|
||||
void reattachToDevice();
|
||||
void updateState();
|
||||
|
||||
~Track();
|
||||
|
||||
private:
|
||||
void finish();
|
||||
void ensureSourceCreated();
|
||||
void playWithLooping(bool looping, float64 volumeOverride);
|
||||
|
||||
not_null<Instance*> _instance;
|
||||
|
||||
bool _failed = false;
|
||||
bool _active = false;
|
||||
bool _looping = false;
|
||||
float64 _volume = 1.;
|
||||
|
||||
int64 _samplesCount = 0;
|
||||
int32 _sampleRate = 0;
|
||||
bytes::vector _samples;
|
||||
|
||||
crl::time _peakDurationMs = 0;
|
||||
int _peakEachPosition = 0;
|
||||
std::vector<uint16> _peaks;
|
||||
uint16 _peakValueMin = 0;
|
||||
uint16 _peakValueMax = 0;
|
||||
|
||||
crl::time _lengthMs = 0;
|
||||
crl::time _stateUpdatedAt = 0;
|
||||
|
||||
int32 _alFormat = 0;
|
||||
int64 _alPosition = 0;
|
||||
uint32 _alSource = 0;
|
||||
uint32 _alBuffer = 0;
|
||||
|
||||
};
|
||||
|
||||
class Instance {
|
||||
public:
|
||||
// Thread: Main.
|
||||
Instance();
|
||||
|
||||
// Thread: Any. Must be locked: AudioMutex.
|
||||
[[nodiscard]] Webrtc::DeviceResolvedId playbackDeviceId() const;
|
||||
|
||||
// Thread: Main.
|
||||
[[nodiscard]] Webrtc::DeviceResolvedId captureDeviceId() const;
|
||||
|
||||
[[nodiscard]] std::unique_ptr<Track> createTrack();
|
||||
|
||||
void detachTracks();
|
||||
void reattachTracks();
|
||||
bool hasActiveTracks() const;
|
||||
|
||||
void scheduleDetachFromDevice();
|
||||
void scheduleDetachIfNotUsed();
|
||||
void stopDetachIfNotUsed();
|
||||
|
||||
~Instance();
|
||||
|
||||
private:
|
||||
friend class Track;
|
||||
void registerTrack(Track *track);
|
||||
void unregisterTrack(Track *track);
|
||||
void trackStarted(Track *track);
|
||||
void trackFinished(Track *track);
|
||||
|
||||
private:
|
||||
std::set<Track*> _tracks;
|
||||
Webrtc::DeviceResolver _playbackDeviceId;
|
||||
Webrtc::DeviceResolver _captureDeviceId;
|
||||
|
||||
base::Timer _updateTimer;
|
||||
|
||||
base::Timer _detachFromDeviceTimer;
|
||||
bool _detachFromDeviceForce = false;
|
||||
|
||||
rpl::lifetime _lifetime;
|
||||
|
||||
};
|
||||
|
||||
[[nodiscard]] Instance &Current();
|
||||
|
||||
} // namespace Audio
|
||||
} // namespace Media
|
||||
113
Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.cpp
Normal file
113
Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#include "media/audio/media_child_ffmpeg_loader.h"
|
||||
|
||||
#include "core/crash_reports.h"
|
||||
#include "core/file_location.h"
|
||||
|
||||
namespace Media {
|
||||
namespace {
|
||||
|
||||
using FFmpeg::AvErrorWrap;
|
||||
|
||||
} // namespace
|
||||
|
||||
ChildFFMpegLoader::ChildFFMpegLoader(
|
||||
std::unique_ptr<ExternalSoundData> &&data)
|
||||
: AbstractAudioFFMpegLoader(
|
||||
Core::FileLocation(),
|
||||
QByteArray(),
|
||||
bytes::vector())
|
||||
, _parentData(std::move(data)) {
|
||||
Expects(_parentData->codec != nullptr);
|
||||
}
|
||||
|
||||
bool ChildFFMpegLoader::open(crl::time positionMs, float64 speed) {
|
||||
const auto sample = (positionMs * samplesFrequency()) / 1000LL;
|
||||
overrideDuration(sample, _parentData->duration);
|
||||
return initUsingContext(_parentData->codec.get(), speed);
|
||||
}
|
||||
|
||||
auto ChildFFMpegLoader::readFromInitialFrame() -> ReadResult {
|
||||
if (!_parentData->frame) {
|
||||
return ReadError::Wait;
|
||||
}
|
||||
return replaceFrameAndRead(base::take(_parentData->frame));
|
||||
}
|
||||
|
||||
auto ChildFFMpegLoader::readMore() -> ReadResult {
|
||||
if (_readTillEnd) {
|
||||
return ReadError::EndOfFile;
|
||||
}
|
||||
const auto initialFrameResult = readFromInitialFrame();
|
||||
if (initialFrameResult != ReadError::Wait) {
|
||||
return initialFrameResult;
|
||||
}
|
||||
|
||||
const auto readResult = readFromReadyContext(
|
||||
_parentData->codec.get());
|
||||
if (readResult != ReadError::Wait) {
|
||||
return readResult;
|
||||
}
|
||||
|
||||
if (_queue.empty()) {
|
||||
if (!_eofReached) {
|
||||
return ReadError::Wait;
|
||||
}
|
||||
_readTillEnd = true;
|
||||
return ReadError::EndOfFile;
|
||||
}
|
||||
|
||||
auto packet = std::move(_queue.front());
|
||||
_queue.pop_front();
|
||||
|
||||
_eofReached = packet.empty();
|
||||
if (_eofReached) {
|
||||
avcodec_send_packet(_parentData->codec.get(), nullptr); // drain
|
||||
return ReadError::Retry;
|
||||
}
|
||||
|
||||
AvErrorWrap error = avcodec_send_packet(
|
||||
_parentData->codec.get(),
|
||||
&packet.fields());
|
||||
if (error) {
|
||||
LogError(u"avcodec_send_packet"_q, error);
|
||||
// There is a sample voice message where skipping such packet
|
||||
// results in a crash (read_access to nullptr) in swr_convert().
|
||||
if (error.code() == AVERROR_INVALIDDATA) {
|
||||
return ReadError::Retry; // try to skip bad packet
|
||||
}
|
||||
return ReadError::Other;
|
||||
}
|
||||
return ReadError::Retry;
|
||||
}
|
||||
|
||||
void ChildFFMpegLoader::enqueuePackets(
|
||||
std::deque<FFmpeg::Packet> &&packets) {
|
||||
if (_queue.empty()) {
|
||||
_queue = std::move(packets);
|
||||
} else {
|
||||
_queue.insert(
|
||||
end(_queue),
|
||||
std::make_move_iterator(packets.begin()),
|
||||
std::make_move_iterator(packets.end()));
|
||||
}
|
||||
packets.clear();
|
||||
}
|
||||
|
||||
void ChildFFMpegLoader::setForceToBuffer(bool force) {
|
||||
_forceToBuffer = force;
|
||||
}
|
||||
|
||||
bool ChildFFMpegLoader::forceToBuffer() const {
|
||||
return _forceToBuffer;
|
||||
}
|
||||
|
||||
ChildFFMpegLoader::~ChildFFMpegLoader() = default;
|
||||
|
||||
} // namespace Media
|
||||
62
Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.h
Normal file
62
Telegram/SourceFiles/media/audio/media_child_ffmpeg_loader.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
This file is part of Telegram Desktop,
|
||||
the official desktop application for the Telegram messaging service.
|
||||
|
||||
For license and copyright information please follow this link:
|
||||
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "media/audio/media_audio_ffmpeg_loader.h"
|
||||
#include "media/streaming/media_streaming_utility.h"
|
||||
|
||||
namespace Media {
|
||||
|
||||
struct ExternalSoundData {
|
||||
FFmpeg::CodecPointer codec;
|
||||
FFmpeg::FramePointer frame;
|
||||
crl::time duration = 0;
|
||||
float64 speed = 1.; // 0.5 <= speed <= 2.
|
||||
};
|
||||
|
||||
struct ExternalSoundPart {
|
||||
AudioMsgId audio;
|
||||
gsl::span<FFmpeg::Packet> packets;
|
||||
};
|
||||
|
||||
class ChildFFMpegLoader : public AbstractAudioFFMpegLoader {
|
||||
public:
|
||||
ChildFFMpegLoader(std::unique_ptr<ExternalSoundData> &&data);
|
||||
|
||||
bool open(crl::time positionMs, float64 speed = 1.) override;
|
||||
|
||||
bool check(const Core::FileLocation &file, const QByteArray &data) override {
|
||||
return true;
|
||||
}
|
||||
|
||||
ReadResult readMore() override;
|
||||
void enqueuePackets(std::deque<FFmpeg::Packet> &&packets) override;
|
||||
void setForceToBuffer(bool force) override;
|
||||
bool forceToBuffer() const override;
|
||||
|
||||
bool eofReached() const {
|
||||
return _eofReached;
|
||||
}
|
||||
|
||||
~ChildFFMpegLoader();
|
||||
|
||||
private:
|
||||
// Streaming player reads first frame by itself and provides it together
|
||||
// with the codec context. So we first read data from this frame and
|
||||
// only after that we try to read next packets.
|
||||
ReadResult readFromInitialFrame();
|
||||
|
||||
std::unique_ptr<ExternalSoundData> _parentData;
|
||||
std::deque<FFmpeg::Packet> _queue;
|
||||
bool _forceToBuffer = false;
|
||||
bool _eofReached = false;
|
||||
bool _readTillEnd = false;
|
||||
|
||||
};
|
||||
|
||||
} // namespace Media
|
||||
Reference in New Issue
Block a user