2017-05-03 11:36:39 +00:00
|
|
|
/*
|
|
|
|
This file is part of Telegram Desktop,
|
|
|
|
the official desktop version of Telegram messaging app, see https://telegram.org
|
|
|
|
|
|
|
|
Telegram Desktop is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
It is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
In addition, as a special exception, the copyright holders give permission
|
|
|
|
to link the code of portions of this program with the OpenSSL library.
|
|
|
|
|
|
|
|
Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
|
|
|
|
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
|
|
|
|
*/
|
|
|
|
#include "media/media_audio_track.h"
|
|
|
|
|
|
|
|
#include "media/media_audio_ffmpeg_loader.h"
|
|
|
|
#include "media/media_audio.h"
|
|
|
|
#include "messenger.h"
|
|
|
|
|
|
|
|
#include <AL/al.h>
|
|
|
|
#include <AL/alc.h>
|
|
|
|
#include <AL/alext.h>
|
|
|
|
|
|
|
|
namespace Media {
|
|
|
|
namespace Audio {
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
constexpr auto kMaxFileSize = 10 * 1024 * 1024;
|
|
|
|
constexpr auto kDetachDeviceTimeout = TimeMs(500); // destroy the audio device after 500ms of silence
|
2017-05-03 13:01:15 +00:00
|
|
|
constexpr auto kTrackUpdateTimeout = TimeMs(100);
|
2017-05-03 11:36:39 +00:00
|
|
|
|
|
|
|
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(gsl::not_null<Instance*> instance) : _instance(instance) {
|
|
|
|
_instance->registerTrack(this);
|
|
|
|
}
|
|
|
|
|
2017-05-07 19:09:20 +00:00
|
|
|
void Track::samplePeakEach(TimeMs peakDuration) {
|
|
|
|
_peakDurationMs = peakDuration;
|
|
|
|
}
|
|
|
|
|
2017-05-03 11:36:39 +00:00
|
|
|
void Track::fillFromData(base::byte_vector &&data) {
|
|
|
|
FFMpegLoader loader(FileLocation(), QByteArray(), std::move(data));
|
|
|
|
|
|
|
|
auto position = qint64(0);
|
|
|
|
if (!loader.open(position)) {
|
|
|
|
_failed = true;
|
|
|
|
return;
|
|
|
|
}
|
2017-05-07 19:09:20 +00:00
|
|
|
auto format = loader.format();
|
|
|
|
_peakEachPosition = _peakDurationMs ? ((loader.samplesFrequency() * _peakDurationMs) / 1000) : 0;
|
|
|
|
auto peaksCount = _peakEachPosition ? (loader.samplesCount() / _peakEachPosition) : 0;
|
|
|
|
_peaks.reserve(peaksCount);
|
|
|
|
auto peakValue = uint16(0);
|
|
|
|
auto peakSamples = 0;
|
|
|
|
auto peakEachSample = (format == AL_FORMAT_STEREO8 || format == AL_FORMAT_STEREO16) ? (_peakEachPosition * 2) : _peakEachPosition;
|
|
|
|
_peakValueMin = 0x7FFF;
|
|
|
|
_peakValueMax = 0;
|
|
|
|
auto peakCallback = [this, &peakValue, &peakSamples, peakEachSample](uint16 sample) {
|
|
|
|
accumulate_max(peakValue, sample);
|
|
|
|
if (++peakSamples >= peakEachSample) {
|
|
|
|
peakSamples -= peakEachSample;
|
|
|
|
_peaks.push_back(peakValue);
|
|
|
|
accumulate_max(_peakValueMax, peakValue);
|
|
|
|
accumulate_min(_peakValueMin, peakValue);
|
|
|
|
peakValue = 0;
|
|
|
|
}
|
|
|
|
};
|
2017-05-03 11:36:39 +00:00
|
|
|
do {
|
|
|
|
auto buffer = QByteArray();
|
2017-05-07 19:09:20 +00:00
|
|
|
auto samplesAdded = int64(0);
|
2017-05-03 11:36:39 +00:00
|
|
|
auto result = loader.readMore(buffer, samplesAdded);
|
|
|
|
if (samplesAdded > 0) {
|
2017-05-07 19:09:20 +00:00
|
|
|
auto sampleBytes = gsl::as_bytes(gsl::make_span(buffer));
|
2017-05-03 11:36:39 +00:00
|
|
|
_samplesCount += samplesAdded;
|
2017-05-07 19:09:20 +00:00
|
|
|
_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);
|
|
|
|
}
|
|
|
|
}
|
2017-05-03 11:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
using Result = AudioPlayerLoader::ReadResult;
|
|
|
|
switch (result) {
|
|
|
|
case Result::Error:
|
|
|
|
case Result::NotYet:
|
|
|
|
case Result::Wait: {
|
|
|
|
_failed = true;
|
|
|
|
} break;
|
|
|
|
}
|
|
|
|
if (result != Result::Ok) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} while (true);
|
|
|
|
|
|
|
|
_alFormat = loader.format();
|
2017-05-03 13:01:15 +00:00
|
|
|
_sampleRate = loader.samplesFrequency();
|
|
|
|
_lengthMs = (loader.samplesCount() * TimeMs(1000)) / _sampleRate;
|
2017-05-03 11:36:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Track::fillFromFile(const 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 = base::byte_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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-03 13:01:15 +00:00
|
|
|
void Track::playWithLooping(bool looping) {
|
|
|
|
_active = true;
|
2017-05-03 11:36:39 +00:00
|
|
|
if (failed() || _samples.empty()) {
|
2017-05-03 13:01:15 +00:00
|
|
|
finish();
|
2017-05-03 11:36:39 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-05-03 13:01:15 +00:00
|
|
|
ensureSourceCreated();
|
2017-05-03 11:36:39 +00:00
|
|
|
alSourceStop(_alSource);
|
2017-05-03 13:01:15 +00:00
|
|
|
_looping = looping;
|
|
|
|
alSourcei(_alSource, AL_LOOPING, _looping ? 1 : 0);
|
2017-05-03 13:58:58 +00:00
|
|
|
alSourcef(_alSource, AL_GAIN, _volume);
|
2017-05-03 11:36:39 +00:00
|
|
|
alSourcePlay(_alSource);
|
2017-05-03 13:01:15 +00:00
|
|
|
_instance->trackStarted(this);
|
2017-05-03 11:36:39 +00:00
|
|
|
}
|
|
|
|
|
2017-05-03 13:01:15 +00:00
|
|
|
void Track::finish() {
|
|
|
|
if (_active) {
|
|
|
|
_active = false;
|
|
|
|
_instance->trackFinished(this);
|
2017-05-03 11:36:39 +00:00
|
|
|
}
|
2017-05-03 13:01:15 +00:00
|
|
|
_alPosition = 0;
|
2017-05-03 11:36:39 +00:00
|
|
|
}
|
|
|
|
|
2017-05-03 13:01:15 +00:00
|
|
|
void Track::ensureSourceCreated() {
|
2017-05-03 11:36:39 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2017-05-07 19:09:20 +00:00
|
|
|
_stateUpdatedAt = getms();
|
2017-05-03 11:36:39 +00:00
|
|
|
auto state = ALint(0);
|
|
|
|
alGetSourcei(_alSource, AL_SOURCE_STATE, &state);
|
|
|
|
if (state != AL_PLAYING) {
|
2017-05-03 13:01:15 +00:00
|
|
|
finish();
|
2017-05-03 11:36:39 +00:00
|
|
|
} else {
|
|
|
|
auto currentPosition = ALint(0);
|
|
|
|
alGetSourcei(_alSource, AL_SAMPLE_OFFSET, ¤tPosition);
|
|
|
|
_alPosition = currentPosition;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-07 19:09:20 +00:00
|
|
|
float64 Track::getPeakValue(TimeMs when) const {
|
|
|
|
if (!isActive() || !_samplesCount || _peaks.empty() || _peakValueMin == _peakValueMax) {
|
|
|
|
return 0.;
|
|
|
|
}
|
|
|
|
auto sampleIndex = (_alPosition + ((when - _stateUpdatedAt) * _sampleRate / 1000));
|
|
|
|
while (sampleIndex < 0) {
|
|
|
|
sampleIndex += _samplesCount;
|
|
|
|
}
|
|
|
|
sampleIndex = sampleIndex % _samplesCount;
|
|
|
|
auto peakIndex = (sampleIndex / _peakEachPosition) % _peaks.size();
|
|
|
|
return (_peaks[peakIndex] - _peakValueMin) / float64(_peakValueMax - _peakValueMin);
|
|
|
|
}
|
|
|
|
|
2017-05-03 11:36:39 +00:00
|
|
|
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;
|
|
|
|
}
|
2017-05-03 13:01:15 +00:00
|
|
|
ensureSourceCreated();
|
2017-05-03 11:36:39 +00:00
|
|
|
|
|
|
|
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() {
|
|
|
|
_updateTimer.setCallback([this] {
|
|
|
|
auto hasActive = false;
|
|
|
|
for (auto track : _tracks) {
|
|
|
|
track->updateState();
|
|
|
|
if (track->isActive()) {
|
|
|
|
hasActive = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (hasActive) {
|
|
|
|
Audio::StopDetachIfNotUsedSafe();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
_detachFromDeviceTimer.setCallback([this] {
|
|
|
|
_detachFromDeviceForce = false;
|
|
|
|
Player::internal::DetachFromDevice();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-05-03 13:01:15 +00:00
|
|
|
void Instance::trackStarted(Track *track) {
|
|
|
|
stopDetachIfNotUsed();
|
|
|
|
if (!_updateTimer.isActive()) {
|
|
|
|
_updateTimer.callEach(kTrackUpdateTimeout);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void Instance::trackFinished(Track *track) {
|
|
|
|
if (!hasActiveTracks()) {
|
|
|
|
_updateTimer.cancel();
|
|
|
|
scheduleDetachIfNotUsed();
|
|
|
|
}
|
|
|
|
if (track->isLooping()) {
|
|
|
|
trackFinished().notify(track, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-03 11:36:39 +00:00
|
|
|
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 Messenger::Instance().audio();
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace Audio
|
|
|
|
} // namespace Media
|