diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fef07ecd..3c5860a8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/audio/audio.h b/src/audio/audio.h index d917bb89a..ac76b40e1 100644 --- a/src/audio/audio.h +++ b/src/audio/audio.h @@ -23,39 +23,12 @@ #include -#include - +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 diff --git a/src/audio/backend/alsink.cpp b/src/audio/backend/alsink.cpp new file mode 100644 index 000000000..97cfc51a7 --- /dev/null +++ b/src/audio/backend/alsink.cpp @@ -0,0 +1,94 @@ +#include "src/audio/backend/alsink.h" +#include "src/audio/backend/openal.h" + +#include +#include + +/** + * @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; +} diff --git a/src/audio/backend/alsink.h b/src/audio/backend/alsink.h new file mode 100644 index 000000000..b29001aaa --- /dev/null +++ b/src/audio/backend/alsink.h @@ -0,0 +1,40 @@ +#ifndef ALSINK_H +#define ALSINK_H + +#include +#include + +#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 diff --git a/src/audio/backend/openal.cpp b/src/audio/backend/openal.cpp index 8041a086a..1e97dc2c7 100644 --- a/src/audio/backend/openal.cpp +++ b/src/audio/backend/openal.cpp @@ -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(&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(&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(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 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 diff --git a/src/audio/backend/openal.h b/src/audio/backend/openal.h index 9ff4a783a..854f87e40 100644 --- a/src/audio/backend/openal.h +++ b/src/audio/backend/openal.h @@ -22,6 +22,10 @@ #define OPENAL_H #include "src/audio/audio.h" +#include "src/audio/backend/alsink.h" + +#include +#include #include #include @@ -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 peerSources; + // Qt containers need copy operators, so use stdlib containers + std::unordered_set sinks; + std::unordered_set soundSinks; + + // number of output sources + int outCount = 0; + int channels = 0; qreal gain = 0; qreal gainFactor = 1; diff --git a/src/audio/backend/openal2.cpp b/src/audio/backend/openal2.cpp index 3c174307b..66a11a56a 100644 --- a/src/audio/backend/openal2.cpp +++ b/src/audio/backend/openal2.cpp @@ -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; diff --git a/src/audio/backend/openal2.h b/src/audio/backend/openal2.h index 7de3de6e9..9f9c6c3a2 100644 --- a/src/audio/backend/openal2.h +++ b/src/audio/backend/openal2.h @@ -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 #include diff --git a/src/audio/iaudiosink.h b/src/audio/iaudiosink.h index 1fb1a9e61..9ddbec7f5 100644 --- a/src/audio/iaudiosink.h +++ b/src/audio/iaudiosink.h @@ -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 {}; diff --git a/src/core/coreav.cpp b/src/core/coreav.cpp index 6c9adc681..7defbcecc 100644 --- a/src/core/coreav.cpp +++ b/src/core/coreav.cpp @@ -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, diff --git a/src/core/coreav.h b/src/core/coreav.h index 51442bc98..a9bd67225 100644 --- a/src/core/coreav.h +++ b/src/core/coreav.h @@ -62,7 +62,6 @@ public: uint32_t rate) const; VideoSource* getVideoSourceFromCall(int callNumber) const; - void invalidateCallSources(); void sendNoVideo(); void joinGroupCall(int groupNum); diff --git a/src/core/toxcall.cpp b/src/core/toxcall.cpp index 0cdc9507e..b9629f319 100644 --- a/src/core/toxcall.cpp +++ b/src/core/toxcall.cpp @@ -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 &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(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 &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; } diff --git a/src/core/toxcall.h b/src/core/toxcall.h index e863ece85..69b132af6 100644 --- a/src/core/toxcall.h +++ b/src/core/toxcall.h @@ -1,6 +1,7 @@ #ifndef TOXCALL_H #define TOXCALL_H +#include "src/audio/iaudiosink.h" #include #include @@ -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 &getAudioSink() const; protected: std::unique_ptr 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 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 &getAudioSink(ToxPk peer); private: - QMap peers; + std::map> peers; + std::map 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 diff --git a/src/widget/form/settings/avform.cpp b/src/widget/form/settings/avform.cpp index c380267f3..25b1b271e 100644 --- a/src/widget/form/settings/avform.cpp +++ b/src/widget/form/settings/avform.cpp @@ -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) diff --git a/src/widget/form/settings/avform.h b/src/widget/form/settings/avform.h index 9cc0dc755..692e91c6f 100644 --- a/src/widget/form/settings/avform.h +++ b/src/widget/form/settings/avform.h @@ -28,13 +28,15 @@ #include "ui_avform.h" #include "src/video/videomode.h" +#include + 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 audioSink = nullptr; VideoSurface* camVideoSurface; CameraSource& camera; QVector> videoDeviceList; diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp index 230dcaa3c..e345a5c74 100644 --- a/src/widget/widget.cpp +++ b/src/widget/widget.cpp @@ -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(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); } } } diff --git a/src/widget/widget.h b/src/widget/widget.h index e2e5a1db1..1b03ac848 100644 --- a/src/widget/widget.h +++ b/src/widget/widget.h @@ -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 audioNotification = nullptr; Settings& settings; QMap friendWidgets;