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:
parent
c61fcd1f2b
commit
5b908184fc
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
94
src/audio/backend/alsink.cpp
Normal file
94
src/audio/backend/alsink.cpp
Normal 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;
|
||||
}
|
40
src/audio/backend/alsink.h
Normal file
40
src/audio/backend/alsink.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -62,7 +62,6 @@ public:
|
|||
uint32_t rate) const;
|
||||
|
||||
VideoSource* getVideoSourceFromCall(int callNumber) const;
|
||||
void invalidateCallSources();
|
||||
void sendNoVideo();
|
||||
|
||||
void joinGroupCall(int groupNum);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user