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 set(${PROJECT_NAME}_SOURCES
src/audio/audio.cpp src/audio/audio.cpp
src/audio/audio.h src/audio/audio.h
src/audio/backend/alsink.cpp
src/audio/backend/alsink.h
src/audio/backend/openal.cpp src/audio/backend/openal.cpp
src/audio/backend/openal.h src/audio/backend/openal.h
src/audio/iaudiosettings.h src/audio/iaudiosettings.h

View File

@ -23,39 +23,12 @@
#include <QObject> #include <QObject>
#include <cassert> class IAudioSink;
class Audio : public QObject class Audio : public QObject
{ {
Q_OBJECT Q_OBJECT
public: 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(); static Audio& getInstance();
virtual qreal outputVolume() const = 0; virtual qreal outputVolume() const = 0;
@ -86,21 +59,12 @@ public:
virtual QStringList outDeviceNames() = 0; virtual QStringList outDeviceNames() = 0;
virtual QStringList inDeviceNames() = 0; virtual QStringList inDeviceNames() = 0;
virtual void subscribeOutput(uint& sourceId) = 0;
virtual void unsubscribeOutput(uint& sourceId) = 0;
virtual void subscribeInput() = 0; virtual void subscribeInput() = 0;
virtual void unsubscribeInput() = 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 stopActive() = 0;
virtual void playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels, virtual IAudioSink* makeSink() = 0;
int sampleRate) = 0;
protected: protected:
// Public default audio settings // 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"); audioThread->setObjectName("qTox Audio");
QObject::connect(audioThread, &QThread::finished, &voiceTimer, &QTimer::stop); QObject::connect(audioThread, &QThread::finished, &voiceTimer, &QTimer::stop);
QObject::connect(audioThread, &QThread::finished, &captureTimer, &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); QObject::connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater);
moveToThread(audioThread); moveToThread(audioThread);
@ -85,9 +84,11 @@ OpenAL::OpenAL()
// TODO for Qt 5.6+: use qOverload // TODO for Qt 5.6+: use qOverload
connect(audioThread, &QThread::started, &captureTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start)); connect(audioThread, &QThread::started, &captureTimer, static_cast<void (QTimer::*)(void)>(&QTimer::start));
connect(&playMono16Timer, &QTimer::timeout, this, &OpenAL::playMono16SoundCleanup); cleanupTimer.setInterval(1000);
playMono16Timer.setSingleShot(true); cleanupTimer.setSingleShot(false);
playMono16Timer.moveToThread(audioThread); 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(); audioThread->start();
} }
@ -222,8 +223,82 @@ void OpenAL::reinitInput(const QString& inDevDesc)
bool OpenAL::reinitOutput(const QString& outDevDesc) bool OpenAL::reinitOutput(const QString& outDevDesc)
{ {
QMutexLocker locker(&audioLock); QMutexLocker locker(&audioLock);
const auto bakSinks = sinks;
sinks.clear();
cleanupOutput(); 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) bool OpenAL::initOutput(const QString& deviceName)
{ {
peerSources.clear(); // there should be no sinks when initializing the output
assert(sinks.size() == 0);
outputInitialized = false; outputInitialized = false;
if (!Settings::getInstance().getAudioOutDevEnabled()) if (!Settings::getInstance().getAudioOutDevEnabled())
@ -359,61 +435,77 @@ bool OpenAL::initOutput(const QString& deviceName)
return false; return false;
} }
alGenSources(1, &alMainSource);
checkAlError();
// init master volume // init master volume
alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f); alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f);
checkAlError(); checkAlError();
Core* core = Core::getInstance();
if (core) {
// reset each call's audio source
core->getAv()->invalidateCallSources();
}
outputInitialized = true; outputInitialized = true;
return 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 * @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); QMutexLocker locker(&audioLock);
if (!autoInitOutput()) auto sinkIt = soundSinks.begin();
return; while (sinkIt != soundSinks.end()) {
auto sink = *sinkIt;
ALuint sourceId = sink->getSourceId();
ALint state = 0;
if (!alMainBuffer) alGetSourcei(sourceId, AL_SOURCE_STATE, &state);
alGenBuffers(1, &alMainBuffer); if (state != AL_PLAYING) {
sinkIt = soundSinks.erase(sinkIt);
ALint state; emit sink->finishedPlaying();
alGetSourcei(alMainSource, AL_SOURCE_STATE, &state); } else {
if (state == AL_PLAYING) { ++sinkIt;
alSourceStop(alMainSource); }
alSourcei(alMainSource, AL_BUFFER, AL_NONE);
} }
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, void OpenAL::playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels,
@ -483,15 +575,6 @@ void OpenAL::cleanupOutput()
outputInitialized = false; outputInitialized = false;
if (alOutDev) { if (alOutDev) {
alSourcei(alMainSource, AL_LOOPING, AL_FALSE);
alSourceStop(alMainSource);
alDeleteSources(1, &alMainSource);
if (alMainBuffer) {
alDeleteBuffers(1, &alMainBuffer);
alMainBuffer = 0;
}
if (!alcMakeContextCurrent(nullptr)) { if (!alcMakeContextCurrent(nullptr)) {
qWarning("Failed to clear audio context."); 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 * @brief Called by doCapture to calculate volume of the audio buffer
* *
@ -605,10 +665,7 @@ void OpenAL::doAudio()
{ {
QMutexLocker lock(&audioLock); QMutexLocker lock(&audioLock);
// Output section // Output section does nothing
if (outputInitialized && !peerSources.isEmpty()) {
doOutput();
}
// Input section // Input section
if (alInDev && inSubscriptions) { if (alInDev && inSubscriptions) {
@ -664,72 +721,36 @@ QStringList OpenAL::inDeviceNames()
return list; return list;
} }
void OpenAL::subscribeOutput(uint& sid) /**
{ * @brief Free all buffers that finished playing on a source
QMutexLocker locker(&audioLock); * @param sourceId where to remove the buffers from
*/
if (!autoInitOutput()) { void OpenAL::cleanupBuffers(uint sourceId) {
qWarning("Failed to subscribe to audio output device."); // unqueue all buffers from the source
return; ALint processed = 0;
} alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed);
std::vector<ALuint> bufids;
alGenSources(1, &sid); // should never be out of range, just to be sure
assert(sid); assert(processed >= 0);
peerSources << sid; assert(processed <= SIZE_MAX);
bufids.resize(processed);
qDebug() << "Audio source" << sid << "created. Sources active:" << peerSources.size(); 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); QMutexLocker locker(&audioLock);
alSourcei(sourceId, AL_LOOPING, AL_TRUE);
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();
} }
void OpenAL::startLoop() void OpenAL::stopLoop(uint sourceId)
{ {
QMutexLocker locker(&audioLock); QMutexLocker locker(&audioLock);
alSourcei(alMainSource, AL_LOOPING, AL_TRUE); alSourcei(sourceId, AL_LOOPING, AL_FALSE);
} alSourceStop(sourceId);
cleanupBuffers(sourceId);
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;
}
} }
qreal OpenAL::inputGain() const qreal OpenAL::inputGain() const

View File

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

View File

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

View File

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

View File

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

View File

@ -502,12 +502,7 @@ void CoreAV::groupCallCallback(void* tox, uint32_t group, uint32_t peer, const i
return; return;
} }
Audio& audio = Audio::getInstance(); call.getAudioSink(peerPk)->playAudioBuffer(data, samples, channels, sample_rate);
if(!call.havePeer(peerPk)) {
call.addPeer(peerPk);
}
audio.playAudioBuffer(call.getAlSource(peerPk), data, samples, channels, sample_rate);
} }
/** /**
@ -678,21 +673,6 @@ bool CoreAV::isCallOutputMuted(const Friend* f) const
return (it != calls.end()) && it->second->getMuteVol(); 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. * @brief Signal to all peers that we're not sending video anymore.
* @note The next frame sent cancels this. * @note The next frame sent cancels this.
@ -886,14 +866,7 @@ void CoreAV::audioFrameCallback(ToxAV*, uint32_t friendNum, const int16_t* pcm,
return; return;
} }
Audio& audio = Audio::getInstance(); call.getAudioSink()->playAudioBuffer(pcm, sampleCount, channels, samplingRate);
if (!call.getAlSource()) {
quint32 sourceId;
audio.subscribeOutput(sourceId);
call.setAlSource(sourceId);
}
audio.playAudioBuffer(call.getAlSource(), pcm, sampleCount, channels, samplingRate);
} }
void CoreAV::videoFrameCallback(ToxAV*, uint32_t friendNum, uint16_t w, uint16_t h, void CoreAV::videoFrameCallback(ToxAV*, uint32_t friendNum, uint16_t w, uint16_t h,

View File

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

View File

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

View File

@ -1,6 +1,7 @@
#ifndef TOXCALL_H #ifndef TOXCALL_H
#define TOXCALL_H #define TOXCALL_H
#include "src/audio/iaudiosink.h"
#include <src/core/toxpk.h> #include <src/core/toxpk.h>
#include <tox/toxav.h> #include <tox/toxav.h>
@ -76,16 +77,21 @@ public:
TOXAV_FRIEND_CALL_STATE getState() const; TOXAV_FRIEND_CALL_STATE getState() const;
void setState(const TOXAV_FRIEND_CALL_STATE& value); void setState(const TOXAV_FRIEND_CALL_STATE& value);
quint32 getAlSource() const; const std::unique_ptr<IAudioSink> &getAudioSink() const;
void setAlSource(const quint32& value);
protected: protected:
std::unique_ptr<QTimer> timeoutTimer; std::unique_ptr<QTimer> timeoutTimer;
private:
QMetaObject::Connection audioSinkInvalid;
void onAudioSourceInvalidated();
void onAudioSinkInvalidated();
private: private:
TOXAV_FRIEND_CALL_STATE state{TOXAV_FRIEND_CALL_STATE_NONE}; TOXAV_FRIEND_CALL_STATE state{TOXAV_FRIEND_CALL_STATE_NONE};
static constexpr int CALL_TIMEOUT = 45000; static constexpr int CALL_TIMEOUT = 45000;
quint32 alSource{0}; std::unique_ptr<IAudioSink> sink = nullptr;
uint32_t friendId;
}; };
class ToxGroupCall : public ToxCall class ToxGroupCall : public ToxCall
@ -103,12 +109,14 @@ public:
bool havePeer(ToxPk peerId); bool havePeer(ToxPk peerId);
void clearPeers(); void clearPeers();
quint32 getAlSource(ToxPk peer); const std::unique_ptr<IAudioSink> &getAudioSink(ToxPk peer);
private: 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 #endif // TOXCALL_H

View File

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

View File

@ -28,13 +28,15 @@
#include "ui_avform.h" #include "ui_avform.h"
#include "src/video/videomode.h" #include "src/video/videomode.h"
#include <memory>
class Audio; class Audio;
class CameraSource; class CameraSource;
class CoreAV; class CoreAV;
class IAudioSettings; class IAudioSettings;
class IVideoSettings; class IVideoSettings;
class VideoSurface; class VideoSurface;
class IAudioSink;
class AVForm : public GenericForm, private Ui::AVForm class AVForm : public GenericForm, private Ui::AVForm
{ {
Q_OBJECT Q_OBJECT
@ -100,6 +102,7 @@ private:
IVideoSettings* videoSettings; IVideoSettings* videoSettings;
bool subscribedToAudioIn; bool subscribedToAudioIn;
std::unique_ptr<IAudioSink> audioSink = nullptr;
VideoSurface* camVideoSurface; VideoSurface* camVideoSurface;
CameraSource& camera; CameraSource& camera;
QVector<QPair<QString, QString>> videoDeviceList; 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) void Widget::incomingNotification(uint32_t friendnumber)
{ {
const auto& friendId = FriendList::id2Key(friendnumber); const auto& friendId = FriendList::id2Key(friendnumber);
newFriendMessageAlert(friendId, false); newFriendMessageAlert(friendId, false);
Audio& audio = Audio::getInstance(); // loop until call answered or rejected
audio.startLoop(); playNotificationSound(IAudioSink::Sound::IncomingCall, true);
audio.playMono16Sound(Audio::getSound(Audio::Sound::IncomingCall));
} }
void Widget::outgoingNotification() void Widget::outgoingNotification()
{ {
Audio& audio = Audio::getInstance(); // loop until call answered or rejected
audio.startLoop(); playNotificationSound(IAudioSink::Sound::OutgoingCall, true);
audio.playMono16Sound(Audio::getSound(Audio::Sound::OutgoingCall));
} }
void Widget::onCallEnd() void Widget::onCallEnd()
{ {
Audio& audio = Audio::getInstance(); playNotificationSound(IAudioSink::Sound::CallEnd);
audio.playMono16Sound(Audio::getSound(Audio::Sound::CallEnd));
} }
/** /**
@ -956,7 +982,7 @@ void Widget::onCallEnd()
*/ */
void Widget::onStopNotification() void Widget::onStopNotification()
{ {
Audio::getInstance().stopLoop(); audioNotification.reset();
} }
void Widget::onRejectCall(uint32_t friendId) void Widget::onRejectCall(uint32_t friendId)
@ -1446,8 +1472,7 @@ bool Widget::newMessageAlert(QWidget* currentWindow, bool isActive, bool sound,
bool notifySound = settings.getNotifySound(); bool notifySound = settings.getNotifySound();
if (notifySound && sound && (!isBusy || busySound)) { if (notifySound && sound && (!isBusy || busySound)) {
QString soundPath = Audio::getSound(Audio::Sound::NewMessage); playNotificationSound(IAudioSink::Sound::NewMessage);
Audio::getInstance().playMono16Sound(soundPath);
} }
} }
} }

View File

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