1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

refactor(audio): move audio output to new IAudioSink interface

This commit is contained in:
sudden6 2019-04-05 01:29:21 +02:00
parent c61fcd1f2b
commit 5b908184fc
No known key found for this signature in database
GPG Key ID: 279509B499E032B9
17 changed files with 469 additions and 300 deletions

View File

@ -236,6 +236,8 @@ execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different
set(${PROJECT_NAME}_SOURCES
src/audio/audio.cpp
src/audio/audio.h
src/audio/backend/alsink.cpp
src/audio/backend/alsink.h
src/audio/backend/openal.cpp
src/audio/backend/openal.h
src/audio/iaudiosettings.h

View File

@ -23,39 +23,12 @@
#include <QObject>
#include <cassert>
class IAudioSink;
class Audio : public QObject
{
Q_OBJECT
public:
enum class Sound
{
NewMessage,
Test,
IncomingCall,
OutgoingCall,
CallEnd
};
inline static QString getSound(Sound s)
{
switch (s) {
case Sound::Test:
return QStringLiteral(":/audio/notification.s16le.pcm");
case Sound::NewMessage:
return QStringLiteral(":/audio/notification.s16le.pcm");
case Sound::IncomingCall:
return QStringLiteral(":/audio/ToxIncomingCall.s16le.pcm");
case Sound::OutgoingCall:
return QStringLiteral(":/audio/ToxOutgoingCall.s16le.pcm");
case Sound::CallEnd:
return QStringLiteral(":/audio/ToxEndCall.s16le.pcm");
}
assert(false);
return QString();
}
static Audio& getInstance();
virtual qreal outputVolume() const = 0;
@ -86,21 +59,12 @@ public:
virtual QStringList outDeviceNames() = 0;
virtual QStringList inDeviceNames() = 0;
virtual void subscribeOutput(uint& sourceId) = 0;
virtual void unsubscribeOutput(uint& sourceId) = 0;
virtual void subscribeInput() = 0;
virtual void unsubscribeInput() = 0;
virtual void startLoop() = 0;
virtual void stopLoop() = 0;
virtual void playMono16Sound(const QByteArray& data) = 0;
virtual void playMono16Sound(const QString& path) = 0;
virtual void stopActive() = 0;
virtual void playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels,
int sampleRate) = 0;
virtual IAudioSink* makeSink() = 0;
protected:
// Public default audio settings

View File

@ -0,0 +1,94 @@
#include "src/audio/backend/alsink.h"
#include "src/audio/backend/openal.h"
#include <QDebug>
#include <QMutexLocker>
/**
* @brief Can play audio via the speakers or some other audio device. Allocates
* and frees the audio ressources internally.
*/
AlSink::~AlSink()
{
QMutexLocker{&killLock};
// unsubscribe only if not already killed
if(!killed) {
audio.destroySink(*this);
killed = true;
}
}
void AlSink::playAudioBuffer(const int16_t *data, int samples, unsigned channels, int sampleRate) const
{
QMutexLocker{&killLock};
if(killed) {
qCritical() << "Trying to play audio on an invalid sink";
} else {
audio.playAudioBuffer(sourceId, data, samples, channels, sampleRate);
}
}
void AlSink::playMono16Sound(const IAudioSink::Sound &sound)
{
QMutexLocker{&killLock};
if(killed) {
qCritical() << "Trying to play sound on an invalid sink";
} else {
audio.playMono16Sound(*this, sound);
}
}
void AlSink::startLoop()
{
QMutexLocker{&killLock};
if(killed) {
qCritical() << "Trying to start loop on an invalid sink";
} else {
audio.startLoop(sourceId);
}
}
void AlSink::stopLoop()
{
QMutexLocker{&killLock};
if(killed) {
qCritical() << "Trying to stop loop on an invalid sink";
} else {
audio.stopLoop(sourceId);
}
}
uint AlSink::getSourceId() const
{
uint tmp = sourceId;
return tmp;
}
void AlSink::kill()
{
killLock.lock();
// this flag is only set once here, afterwards the object is considered dead
killed = true;
killLock.unlock();
emit invalidated();
}
AlSink::AlSink(OpenAL& al, uint sourceId)
: audio{al}
, sourceId{sourceId}
, killLock{QMutex::Recursive}
{
}
AlSink::operator bool() const
{
QMutexLocker{&killLock};
return !killed;
}

View File

@ -0,0 +1,40 @@
#ifndef ALSINK_H
#define ALSINK_H
#include <QObject>
#include <QMutex>
#include "src/audio/iaudiosink.h"
class OpenAL;
class QMutex;
class AlSink : public IAudioSink
{
Q_OBJECT
public:
AlSink(OpenAL &al, uint sourceId);
AlSink(const AlSink &src) = delete;
AlSink & operator=(const AlSink&) = delete;
AlSink(AlSink&& other) = delete;
AlSink & operator=(AlSink&& other) = delete;
~AlSink();
void playAudioBuffer(const int16_t* data, int samples, unsigned channels,
int sampleRate) const;
void playMono16Sound(const IAudioSink::Sound& sound);
void startLoop();
void stopLoop();
operator bool() const;
uint getSourceId() const;
void kill();
private:
OpenAL& audio;
uint sourceId;
bool killed = false;
mutable QMutex killLock;
};
#endif // ALSINK_H

View File

@ -68,7 +68,6 @@ OpenAL::OpenAL()
audioThread->setObjectName("qTox Audio");
QObject::connect(audioThread, &QThread::finished, &voiceTimer, &QTimer::stop);
QObject::connect(audioThread, &QThread::finished, &captureTimer, &QTimer::stop);
QObject::connect(audioThread, &QThread::finished, &playMono16Timer, &QTimer::stop);
QObject::connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater);
moveToThread(audioThread);
@ -85,9 +84,11 @@ OpenAL::OpenAL()
// TODO for Qt 5.6+: use qOverload
connect(audioThread, &QThread::started, &captureTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start));
connect(&playMono16Timer, &QTimer::timeout, this, &OpenAL::playMono16SoundCleanup);
playMono16Timer.setSingleShot(true);
playMono16Timer.moveToThread(audioThread);
cleanupTimer.setInterval(1000);
cleanupTimer.setSingleShot(false);
connect(&cleanupTimer, &QTimer::timeout, this, &OpenAL::cleanupSound);
// TODO for Qt 5.6+: use qOverload
connect(audioThread, &QThread::started, &cleanupTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start));
audioThread->start();
}
@ -222,8 +223,82 @@ void OpenAL::reinitInput(const QString& inDevDesc)
bool OpenAL::reinitOutput(const QString& outDevDesc)
{
QMutexLocker locker(&audioLock);
const auto bakSinks = sinks;
sinks.clear();
cleanupOutput();
return initOutput(outDevDesc);
const bool result = initOutput(outDevDesc);
locker.unlock();
// this must happen outside `audioLock`, to avoid a deadlock when
// a slot on AlSink::invalidate tries to create a new source immedeately.
for (auto& sink : bakSinks) {
sink->kill();
}
return result;
}
/**
* @brief Allocates ressources for a new audio output
* @return AudioSink on success, nullptr on failure
*/
IAudioSink *OpenAL::makeSink()
{
QMutexLocker locker(&audioLock);
if (!autoInitOutput()) {
qWarning("Failed to subscribe to audio output device.");
return {};
}
ALuint sid;
alGenSources(1, &sid);
auto const sink = new AlSink(*this, sid);
if (sink == nullptr) {
return {};
}
sinks.insert(sink);
qDebug() << "Audio source" << sid << "created. Sources active:" << sinks.size();
return sink;
}
/**
* @brief Must be called by the destructor of AlSink to remove the internal ressources.
* If no sinks are opened, the output is closed afterwards.
* @param sink Audio sink to remove.
*/
void OpenAL::destroySink(AlSink& sink)
{
QMutexLocker locker(&audioLock);
const auto sinksErased = sinks.erase(&sink);
const auto soundSinksErased = soundSinks.erase(&sink);
if (sinksErased == 0 && soundSinksErased == 0) {
qWarning() << "Destroying non-existant source";
return;
}
const uint sid = sink.getSourceId();
if (alIsSource(sid)) {
// stop playing, marks all buffers as processed
alSourceStop(sid);
cleanupBuffers(sid);
qDebug() << "Audio source" << sid << "deleted. Sources active:" << sinks.size();
} else {
qWarning() << "Trying to delete invalid audio source" << sid;
}
if (sinks.empty() && soundSinks.empty()) {
cleanupOutput();
}
}
/**
@ -332,7 +407,8 @@ bool OpenAL::initInput(const QString& deviceName, uint32_t channels)
*/
bool OpenAL::initOutput(const QString& deviceName)
{
peerSources.clear();
// there should be no sinks when initializing the output
assert(sinks.size() == 0);
outputInitialized = false;
if (!Settings::getInstance().getAudioOutDevEnabled())
@ -359,61 +435,77 @@ bool OpenAL::initOutput(const QString& deviceName)
return false;
}
alGenSources(1, &alMainSource);
checkAlError();
// init master volume
alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f);
checkAlError();
Core* core = Core::getInstance();
if (core) {
// reset each call's audio source
core->getAv()->invalidateCallSources();
}
outputInitialized = true;
return true;
}
/**
* @brief Play a 48kHz mono 16bit PCM sound from a file
*
* @param[in] path the path to the sound file
*/
void OpenAL::playMono16Sound(const QString& path)
{
QFile sndFile(path);
sndFile.open(QIODevice::ReadOnly);
playMono16Sound(QByteArray(sndFile.readAll()));
}
/**
* @brief Play a 48kHz mono 16bit PCM sound
*/
void OpenAL::playMono16Sound(const QByteArray& data)
void OpenAL::playMono16Sound(AlSink& sink, const IAudioSink::Sound &sound)
{
const uint sourceId = sink.getSourceId();
QFile sndFile(IAudioSink::getSound(sound));
if(!sndFile.exists()) {
qDebug() << "Trying to open non existent sound file";
return;
}
sndFile.open(QIODevice::ReadOnly);
const QByteArray data{sndFile.readAll()};
if(data.isEmpty()) {
qDebug() << "Sound file contained no data";
return;
}
QMutexLocker locker(&audioLock);
// interrupt possibly playing sound, we don't buffer here
alSourceStop(sourceId);
ALint processed = 0;
alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
alSourcei(sourceId, AL_LOOPING, AL_FALSE);
ALuint bufid;
if (processed == 0) {
// create new buffer
alGenBuffers(1, &bufid);
} else {
// we only reserve space for one buffer
assert(processed == 1);
// unqueue all processed buffers
alSourceUnqueueBuffers(sourceId, processed, &bufid);
}
alBufferData(bufid, AL_FORMAT_MONO16, data.constData(), data.size(), AUDIO_SAMPLE_RATE);
alSourcei(sourceId, AL_BUFFER, bufid);
alSourcePlay(sourceId);
soundSinks.insert(&sink);
}
void OpenAL::cleanupSound()
{
QMutexLocker locker(&audioLock);
if (!autoInitOutput())
return;
auto sinkIt = soundSinks.begin();
while (sinkIt != soundSinks.end()) {
auto sink = *sinkIt;
ALuint sourceId = sink->getSourceId();
ALint state = 0;
if (!alMainBuffer)
alGenBuffers(1, &alMainBuffer);
ALint state;
alGetSourcei(alMainSource, AL_SOURCE_STATE, &state);
if (state == AL_PLAYING) {
alSourceStop(alMainSource);
alSourcei(alMainSource, AL_BUFFER, AL_NONE);
alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
if (state != AL_PLAYING) {
sinkIt = soundSinks.erase(sinkIt);
emit sink->finishedPlaying();
} else {
++sinkIt;
}
}
alBufferData(alMainBuffer, AL_FORMAT_MONO16, data.constData(), data.size(), AUDIO_SAMPLE_RATE);
alSourcei(alMainSource, AL_BUFFER, static_cast<ALint>(alMainBuffer));
alSourcePlay(alMainSource);
int durationMs = data.size() * 1000 / 2 / AUDIO_SAMPLE_RATE;
QMetaObject::invokeMethod(&playMono16Timer, "start", Q_ARG(int, durationMs + 50));
}
void OpenAL::playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels,
@ -483,15 +575,6 @@ void OpenAL::cleanupOutput()
outputInitialized = false;
if (alOutDev) {
alSourcei(alMainSource, AL_LOOPING, AL_FALSE);
alSourceStop(alMainSource);
alDeleteSources(1, &alMainSource);
if (alMainBuffer) {
alDeleteBuffers(1, &alMainBuffer);
alMainBuffer = 0;
}
if (!alcMakeContextCurrent(nullptr)) {
qWarning("Failed to clear audio context.");
}
@ -508,29 +591,6 @@ void OpenAL::cleanupOutput()
}
}
/**
* @brief Called after a mono16 sound stopped playing
*/
void OpenAL::playMono16SoundCleanup()
{
QMutexLocker locker(&audioLock);
ALint state;
alGetSourcei(alMainSource, AL_SOURCE_STATE, &state);
if (state == AL_STOPPED) {
alSourcei(alMainSource, AL_BUFFER, AL_NONE);
alDeleteBuffers(1, &alMainBuffer);
alMainBuffer = 0;
// close the audio device if no other sources active
if (peerSources.isEmpty()) {
cleanupOutput();
}
} else {
// the audio didn't finish, try again later
playMono16Timer.start(10);
}
}
/**
* @brief Called by doCapture to calculate volume of the audio buffer
*
@ -605,10 +665,7 @@ void OpenAL::doAudio()
{
QMutexLocker lock(&audioLock);
// Output section
if (outputInitialized && !peerSources.isEmpty()) {
doOutput();
}
// Output section does nothing
// Input section
if (alInDev && inSubscriptions) {
@ -664,72 +721,36 @@ QStringList OpenAL::inDeviceNames()
return list;
}
void OpenAL::subscribeOutput(uint& sid)
{
QMutexLocker locker(&audioLock);
if (!autoInitOutput()) {
qWarning("Failed to subscribe to audio output device.");
return;
}
alGenSources(1, &sid);
assert(sid);
peerSources << sid;
qDebug() << "Audio source" << sid << "created. Sources active:" << peerSources.size();
/**
* @brief Free all buffers that finished playing on a source
* @param sourceId where to remove the buffers from
*/
void OpenAL::cleanupBuffers(uint sourceId) {
// unqueue all buffers from the source
ALint processed = 0;
alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
std::vector<ALuint> bufids;
// should never be out of range, just to be sure
assert(processed >= 0);
assert(processed <= SIZE_MAX);
bufids.resize(processed);
alSourceUnqueueBuffers(sourceId, processed, bufids.data());
// delete all buffers
alDeleteBuffers(processed, bufids.data());
}
void OpenAL::unsubscribeOutput(uint& sid)
void OpenAL::startLoop(uint sourceId)
{
QMutexLocker locker(&audioLock);
peerSources.removeAll(sid);
if (sid) {
if (alIsSource(sid)) {
// stop playing, marks all buffers as processed
alSourceStop(sid);
// unqueue all buffers from the source
ALint processed = 0;
alGetSourcei(sid, AL_BUFFERS_PROCESSED, &processed);
ALuint* bufids = new ALuint[processed];
alSourceUnqueueBuffers(sid, processed, bufids);
// delete all buffers
alDeleteBuffers(processed, bufids);
delete[] bufids;
alDeleteSources(1, &sid);
qDebug() << "Audio source" << sid << "deleted. Sources active:" << peerSources.size();
} else {
qWarning() << "Trying to delete invalid audio source" << sid;
}
sid = 0;
}
if (peerSources.isEmpty())
cleanupOutput();
alSourcei(sourceId, AL_LOOPING, AL_TRUE);
}
void OpenAL::startLoop()
void OpenAL::stopLoop(uint sourceId)
{
QMutexLocker locker(&audioLock);
alSourcei(alMainSource, AL_LOOPING, AL_TRUE);
}
void OpenAL::stopLoop()
{
QMutexLocker locker(&audioLock);
alSourcei(alMainSource, AL_LOOPING, AL_FALSE);
alSourceStop(alMainSource);
ALint state;
alGetSourcei(alMainSource, AL_SOURCE_STATE, &state);
if (state == AL_STOPPED) {
alSourcei(alMainSource, AL_BUFFER, AL_NONE);
alDeleteBuffers(1, &alMainBuffer);
alMainBuffer = 0;
}
alSourcei(sourceId, AL_LOOPING, AL_FALSE);
alSourceStop(sourceId);
cleanupBuffers(sourceId);
}
qreal OpenAL::inputGain() const

View File

@ -22,6 +22,10 @@
#define OPENAL_H
#include "src/audio/audio.h"
#include "src/audio/backend/alsink.h"
#include <memory>
#include <unordered_set>
#include <atomic>
#include <cmath>
@ -76,16 +80,15 @@ public:
QStringList outDeviceNames();
QStringList inDeviceNames();
void subscribeOutput(uint& sourceId);
void unsubscribeOutput(uint& sourceId);
IAudioSink* makeSink();
void destroySink(AlSink& sink);
void subscribeInput();
void unsubscribeInput();
void startLoop();
void stopLoop();
void playMono16Sound(const QByteArray& data);
void playMono16Sound(const QString& path);
void startLoop(uint sourceId);
void stopLoop(uint sourceId);
void playMono16Sound(AlSink& sink, const IAudioSink::Sound& sound);
void stopActive();
void playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels,
@ -113,24 +116,35 @@ protected:
private:
virtual bool initInput(const QString& deviceName);
virtual bool initOutput(const QString& outDevDescr);
void playMono16SoundCleanup();
void cleanupBuffers(uint sourceId);
void cleanupSound();
float getVolume();
protected:
QThread* audioThread;
mutable QMutex audioLock{QMutex::Recursive};
QString inDev{};
QString outDev{};
ALCdevice* alInDev = nullptr;
quint32 inSubscriptions = 0;
QTimer captureTimer, playMono16Timer;
QTimer captureTimer;
QTimer cleanupTimer;
ALCdevice* alOutDev = nullptr;
ALCcontext* alOutContext = nullptr;
ALuint alMainSource = 0;
ALuint alMainBuffer = 0;
bool outputInitialized = false;
QList<ALuint> peerSources;
// Qt containers need copy operators, so use stdlib containers
std::unordered_set<AlSink*> sinks;
std::unordered_set<AlSink*> soundSinks;
// number of output sources
int outCount = 0;
int channels = 0;
qreal gain = 0;
qreal gainFactor = 1;

View File

@ -205,7 +205,7 @@ bool OpenAL2::initOutputEchoCancel()
*/
bool OpenAL2::initOutput(const QString& deviceName)
{
peerSources.clear();
assert(sinks.empty());
outputInitialized = false;
if (!Settings::getInstance().getAudioOutDevEnabled()) {
@ -243,19 +243,10 @@ bool OpenAL2::initOutput(const QString& deviceName)
alProxyContext = alOutContext;
}
alGenSources(1, &alMainSource);
checkAlError();
// init master volume
alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f);
checkAlError();
Core* core = Core::getInstance();
if (core) {
// reset each call's audio source
core->getAv()->invalidateCallSources();
}
// ensure alProxyContext is active
alcMakeContextCurrent(alProxyContext);
outputInitialized = true;

View File

@ -21,8 +21,8 @@
#ifndef OPENAL2_H
#define OPENAL2_H
#include "openal.h"
#include "src/audio/audio.h"
#include "src/audio/backend/openal.h"
#include <atomic>
#include <cmath>

View File

@ -63,15 +63,15 @@ public:
{
switch (s) {
case Sound::Test:
return QStringLiteral(":/audio/notification.pcm");
return QStringLiteral(":/audio/notification.s16le.pcm");
case Sound::NewMessage:
return QStringLiteral(":/audio/notification.pcm");
return QStringLiteral(":/audio/notification.s16le.pcm");
case Sound::IncomingCall:
return QStringLiteral(":/audio/ToxIncomingCall.pcm");
return QStringLiteral(":/audio/ToxIncomingCall.s16le.pcm");
case Sound::OutgoingCall:
return QStringLiteral(":/audio/ToxOutgoingCall.pcm");
return QStringLiteral(":/audio/ToxOutgoingCall.s16le.pcm");
case Sound::CallEnd:
return QStringLiteral(":/audio/ToxEndCall.pcm");
return QStringLiteral(":/audio/ToxEndCall.s16le.pcm");
}
assert(false);
return {};

View File

@ -502,12 +502,7 @@ void CoreAV::groupCallCallback(void* tox, uint32_t group, uint32_t peer, const i
return;
}
Audio& audio = Audio::getInstance();
if(!call.havePeer(peerPk)) {
call.addPeer(peerPk);
}
audio.playAudioBuffer(call.getAlSource(peerPk), data, samples, channels, sample_rate);
call.getAudioSink(peerPk)->playAudioBuffer(data, samples, channels, sample_rate);
}
/**
@ -678,21 +673,6 @@ bool CoreAV::isCallOutputMuted(const Friend* f) const
return (it != calls.end()) && it->second->getMuteVol();
}
/**
* @brief Forces to regenerate each call's audio sources.
*/
void CoreAV::invalidateCallSources()
{
for (auto& kv : groupCalls) {
kv.second->clearPeers();
}
for (auto& kv : calls) {
// TODO: this is wrong, "0" is a valid source id
kv.second->setAlSource(0);
}
}
/**
* @brief Signal to all peers that we're not sending video anymore.
* @note The next frame sent cancels this.
@ -886,14 +866,7 @@ void CoreAV::audioFrameCallback(ToxAV*, uint32_t friendNum, const int16_t* pcm,
return;
}
Audio& audio = Audio::getInstance();
if (!call.getAlSource()) {
quint32 sourceId;
audio.subscribeOutput(sourceId);
call.setAlSource(sourceId);
}
audio.playAudioBuffer(call.getAlSource(), pcm, sampleCount, channels, samplingRate);
call.getAudioSink()->playAudioBuffer(pcm, sampleCount, channels, samplingRate);
}
void CoreAV::videoFrameCallback(ToxAV*, uint32_t friendNum, uint16_t w, uint16_t h,

View File

@ -62,7 +62,6 @@ public:
uint32_t rate) const;
VideoSource* getVideoSourceFromCall(int callNumber) const;
void invalidateCallSources();
void sendNoVideo();
void joinGroupCall(int groupNum);

View File

@ -101,18 +101,10 @@ CoreVideoSource* ToxCall::getVideoSource() const
return videoSource;
}
quint32 ToxFriendCall::getAlSource() const
{
return alSource;
}
void ToxFriendCall::setAlSource(const quint32& value)
{
alSource = value;
}
ToxFriendCall::ToxFriendCall(uint32_t FriendNum, bool VideoEnabled, CoreAV& av)
: ToxCall(VideoEnabled, av)
, sink(Audio::getInstance().makeSink())
, friendId{FriendNum}
{
// register audio
Audio& audio = Audio::getInstance();
@ -127,7 +119,11 @@ ToxFriendCall::ToxFriendCall(uint32_t FriendNum, bool VideoEnabled, CoreAV& av)
qDebug() << "Audio input connection not working";
}
audio.subscribeOutput(alSource);
audioSinkInvalid = QObject::connect(sink.get(), &IAudioSink::invalidated,
[this]() {
this->onAudioSinkInvalidated();
});
// register video
if (videoEnabled) {
@ -150,8 +146,18 @@ ToxFriendCall::ToxFriendCall(uint32_t FriendNum, bool VideoEnabled, CoreAV& av)
ToxFriendCall::~ToxFriendCall()
{
auto& audio = Audio::getInstance();
audio.unsubscribeOutput(alSource);
QObject::disconnect(audioSinkInvalid);
}
void ToxFriendCall::onAudioSinkInvalidated()
{
const auto newSink = Audio::getInstance().makeSink();
audioSinkInvalid = QObject::connect(newSink, &IAudioSink::invalidated,
[this]() {
this->onAudioSinkInvalidated();
});
sink.reset(newSink);
}
void ToxFriendCall::startTimeout(uint32_t callId)
@ -187,8 +193,14 @@ void ToxFriendCall::setState(const TOXAV_FRIEND_CALL_STATE& value)
state = value;
}
const std::unique_ptr<IAudioSink> &ToxFriendCall::getAudioSink() const
{
return sink;
}
ToxGroupCall::ToxGroupCall(int GroupNum, CoreAV& av)
: ToxCall(false, av)
, groupId{GroupNum}
{
// register audio
Audio& audio = Audio::getInstance();
@ -206,57 +218,64 @@ ToxGroupCall::ToxGroupCall(int GroupNum, CoreAV& av)
ToxGroupCall::~ToxGroupCall()
{
Audio& audio = Audio::getInstance();
// disconnect all Qt connections
clearPeers();
}
for (quint32 v : peers) {
audio.unsubscribeOutput(v);
}
void ToxGroupCall::onAudioSinkInvalidated(ToxPk peerId)
{
removePeer(peerId);
addPeer(peerId);
}
void ToxGroupCall::removePeer(ToxPk peerId)
{
auto& audio = Audio::getInstance();
const auto& sourceId = peers.find(peerId);
if(sourceId == peers.constEnd()) {
qDebug() << "Peer does not have a source, can't remove";
const auto& source = peers.find(peerId);
if(source == peers.cend()) {
qDebug() << "Peer:" << peerId.toString() << "does not have a source, can't remove";
return;
}
audio.unsubscribeOutput(sourceId.value());
peers.remove(peerId);
peers.erase(source);
QObject::disconnect(sinkInvalid[peerId]);
sinkInvalid.erase(peerId);
}
void ToxGroupCall::addPeer(ToxPk peerId)
{
auto& audio = Audio::getInstance();
uint sourceId = 0;
audio.subscribeOutput(sourceId);
peers.insert(peerId, sourceId);
IAudioSink* newSink = audio.makeSink();
peers.emplace(peerId, std::unique_ptr<IAudioSink>(newSink));
QMetaObject::Connection con = QObject::connect(newSink, &IAudioSink::invalidated,
[this, peerId]() {
this->onAudioSinkInvalidated(peerId);
});
sinkInvalid.insert({peerId, con});
}
bool ToxGroupCall::havePeer(ToxPk peerId)
{
const auto& sourceId = peers.find(peerId);
return sourceId != peers.constEnd();
const auto& source = peers.find(peerId);
return source != peers.cend();
}
void ToxGroupCall::clearPeers()
{
Audio& audio = Audio::getInstance();
for (quint32 v : peers) {
audio.unsubscribeOutput(v);
peers.clear();
for(auto con : sinkInvalid) {
QObject::disconnect(con.second);
}
peers.clear();
sinkInvalid.clear();
}
quint32 ToxGroupCall::getAlSource(ToxPk peer)
const std::unique_ptr<IAudioSink> &ToxGroupCall::getAudioSink(ToxPk peer)
{
if(!havePeer(peer)) {
qWarning() << "Getting alSource for non-existant peer";
return 0;
addPeer(peer);
}
return peers[peer];
const auto& source = peers.find(peer);
return source->second;
}

View File

@ -1,6 +1,7 @@
#ifndef TOXCALL_H
#define TOXCALL_H
#include "src/audio/iaudiosink.h"
#include <src/core/toxpk.h>
#include <tox/toxav.h>
@ -76,16 +77,21 @@ public:
TOXAV_FRIEND_CALL_STATE getState() const;
void setState(const TOXAV_FRIEND_CALL_STATE& value);
quint32 getAlSource() const;
void setAlSource(const quint32& value);
const std::unique_ptr<IAudioSink> &getAudioSink() const;
protected:
std::unique_ptr<QTimer> timeoutTimer;
private:
QMetaObject::Connection audioSinkInvalid;
void onAudioSourceInvalidated();
void onAudioSinkInvalidated();
private:
TOXAV_FRIEND_CALL_STATE state{TOXAV_FRIEND_CALL_STATE_NONE};
static constexpr int CALL_TIMEOUT = 45000;
quint32 alSource{0};
std::unique_ptr<IAudioSink> sink = nullptr;
uint32_t friendId;
};
class ToxGroupCall : public ToxCall
@ -103,12 +109,14 @@ public:
bool havePeer(ToxPk peerId);
void clearPeers();
quint32 getAlSource(ToxPk peer);
const std::unique_ptr<IAudioSink> &getAudioSink(ToxPk peer);
private:
QMap<ToxPk, quint32> peers;
std::map<ToxPk, std::unique_ptr<IAudioSink>> peers;
std::map<ToxPk, QMetaObject::Connection> sinkInvalid;
int groupId;
// If you add something here, don't forget to override the ctors and move operators!
void onAudioSourceInvalidated();
void onAudioSinkInvalidated(ToxPk peerId);
};
#endif // TOXCALL_H

View File

@ -122,11 +122,12 @@ void AVForm::hideEvent(QHideEvent* event)
{
if (subscribedToAudioIn) {
// TODO: This should not be done in show/hide events
audio->unsubscribeOutput(alSource);
audio->unsubscribeInput();
subscribedToAudioIn = false;
}
audioSink.reset();
if (camVideoSurface) {
camVideoSurface->setSource(nullptr);
killVideoSurface();
@ -145,11 +146,14 @@ void AVForm::showEvent(QShowEvent* event)
if (!subscribedToAudioIn) {
// TODO: This should not be done in show/hide events
audio->subscribeOutput(alSource);
audio->subscribeInput();
subscribedToAudioIn = true;
}
if(audioSink == nullptr) {
audioSink.reset(audio->makeSink());
}
GenericForm::showEvent(event);
}
@ -560,14 +564,19 @@ void AVForm::on_outDevCombobox_currentIndexChanged(int deviceIndex)
const bool outputEnabled = deviceIndex > 0;
audioSettings->setAudioOutDevEnabled(outputEnabled);
QString deviceName;
QString deviceName{};
if (outputEnabled) {
deviceName = outDevCombobox->itemText(deviceIndex);
}
audioSettings->setOutDev(deviceName);
const QString oldName = audioSettings->getOutDev();
if(oldName != deviceName) {
audioSettings->setOutDev(deviceName);
audio->reinitOutput(deviceName);
audioSink.reset(Audio::getInstance().makeSink());
}
audio->reinitOutput(deviceName);
playbackSlider->setEnabled(outputEnabled);
playbackSlider->setSliderPosition(
getStepsFromValue(audio->outputVolume(), audio->minOutputVolume(), audio->maxOutputVolume()));
@ -584,8 +593,9 @@ void AVForm::on_playbackSlider_valueChanged(int sliderSteps)
getValueFromSteps(sliderSteps, audio->minOutputVolume(), audio->maxOutputVolume());
audio->setOutputVolume(volume);
if (cbEnableTestSound->isChecked())
audio->playMono16Sound(Audio::getSound(Audio::Sound::Test));
if (cbEnableTestSound->isChecked() && audioSink) {
audioSink->playMono16Sound(IAudioSink::Sound::Test);
}
}
}
@ -593,8 +603,9 @@ void AVForm::on_cbEnableTestSound_stateChanged()
{
audioSettings->setEnableTestSound(cbEnableTestSound->isChecked());
if (cbEnableTestSound->isChecked() && audio->isOutputReady())
audio->playMono16Sound(Audio::getSound(Audio::Sound::Test));
if (cbEnableTestSound->isChecked() && audio->isOutputReady() && audioSink) {
audioSink->playMono16Sound(IAudioSink::Sound::Test);
}
}
void AVForm::on_microphoneSlider_valueChanged(int sliderSteps)

View File

@ -28,13 +28,15 @@
#include "ui_avform.h"
#include "src/video/videomode.h"
#include <memory>
class Audio;
class CameraSource;
class CoreAV;
class IAudioSettings;
class IVideoSettings;
class VideoSurface;
class IAudioSink;
class AVForm : public GenericForm, private Ui::AVForm
{
Q_OBJECT
@ -100,6 +102,7 @@ private:
IVideoSettings* videoSettings;
bool subscribedToAudioIn;
std::unique_ptr<IAudioSink> audioSink = nullptr;
VideoSurface* camVideoSurface;
CameraSource& camera;
QVector<QPair<QString, QString>> videoDeviceList;

View File

@ -928,27 +928,53 @@ void Widget::reloadHistory()
}
}
/**
* @brief Plays a sound via the audioNotification AudioSink
* @param sound Sound to play
* @param loop if true, loop the sound until onStopNotification() is called
*/
void Widget::playNotificationSound(IAudioSink::Sound sound, bool loop) {
if(audioNotification == nullptr) {
audioNotification = std::unique_ptr<IAudioSink>(Audio::getInstance().makeSink());
if(audioNotification == nullptr) {
qDebug() << "Failed to allocate AudioSink";
return;
}
}
connect(audioNotification.get(), &IAudioSink::finishedPlaying,
this, &Widget::cleanupNotificationSound);
if(loop) {
audioNotification->startLoop();
}
audioNotification->playMono16Sound(sound);
}
void Widget::cleanupNotificationSound()
{
audioNotification.reset();
}
void Widget::incomingNotification(uint32_t friendnumber)
{
const auto& friendId = FriendList::id2Key(friendnumber);
newFriendMessageAlert(friendId, false);
Audio& audio = Audio::getInstance();
audio.startLoop();
audio.playMono16Sound(Audio::getSound(Audio::Sound::IncomingCall));
// loop until call answered or rejected
playNotificationSound(IAudioSink::Sound::IncomingCall, true);
}
void Widget::outgoingNotification()
{
Audio& audio = Audio::getInstance();
audio.startLoop();
audio.playMono16Sound(Audio::getSound(Audio::Sound::OutgoingCall));
// loop until call answered or rejected
playNotificationSound(IAudioSink::Sound::OutgoingCall, true);
}
void Widget::onCallEnd()
{
Audio& audio = Audio::getInstance();
audio.playMono16Sound(Audio::getSound(Audio::Sound::CallEnd));
playNotificationSound(IAudioSink::Sound::CallEnd);
}
/**
@ -956,7 +982,7 @@ void Widget::onCallEnd()
*/
void Widget::onStopNotification()
{
Audio::getInstance().stopLoop();
audioNotification.reset();
}
void Widget::onRejectCall(uint32_t friendId)
@ -1446,8 +1472,7 @@ bool Widget::newMessageAlert(QWidget* currentWindow, bool isActive, bool sound,
bool notifySound = settings.getNotifySound();
if (notifySound && sound && (!isBusy || busySound)) {
QString soundPath = Audio::getSound(Audio::Sound::NewMessage);
Audio::getInstance().playMono16Sound(soundPath);
playNotificationSound(IAudioSink::Sound::NewMessage);
}
}
}

View File

@ -29,6 +29,7 @@
#include "genericchatitemwidget.h"
#include "src/audio/iaudiosink.h"
#include "src/core/core.h"
#include "src/core/toxfile.h"
#include "src/core/groupid.h"
@ -45,6 +46,7 @@ class MainWindow;
}
class AddFriendForm;
class AlSink;
class Camera;
class ChatForm;
class CircleWidget;
@ -256,6 +258,8 @@ private:
void retranslateUi();
void focusChatInput();
void openDialog(GenericChatroomWidget* widget, bool newWindow);
void playNotificationSound(IAudioSink::Sound sound, bool loop = false);
void cleanupNotificationSound();
private:
SystemTrayIcon* icon = nullptr;
@ -308,6 +312,7 @@ private:
QPushButton* groupInvitesButton;
unsigned int unreadGroupInvites;
int icon_size;
std::unique_ptr<IAudioSink> audioNotification = nullptr;
Settings& settings;
QMap<ToxPk, FriendWidget*> friendWidgets;