From 28c2298ad97e8aec6097d63ead55214bac8152ba Mon Sep 17 00:00:00 2001 From: sudden6 Date: Wed, 12 Apr 2017 23:27:48 +0200 Subject: [PATCH] feat(audio): split the audio interface from the backend library This paves the way to support multiple audio backends --- CMakeLists.txt | 2 + src/audio/audio.cpp | 760 ++++------------------------ src/audio/audio.h | 101 +--- src/audio/backend/openal.cpp | 634 +++++++++++++++++++++++ src/audio/backend/openal.h | 127 +++++ src/widget/form/settings/avform.cpp | 4 +- 6 files changed, 894 insertions(+), 734 deletions(-) create mode 100644 src/audio/backend/openal.cpp create mode 100644 src/audio/backend/openal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 939f99dd7..91dd39c5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,6 +179,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/openal.cpp + src/audio/backend/openal.h src/chatlog/chatlinecontent.cpp src/chatlog/chatlinecontent.h src/chatlog/chatlinecontentproxy.cpp diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index a98b812cf..9d6bef502 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -1,5 +1,5 @@ /* - Copyright © 2014-2015 by The qTox Project Contributors + Copyright © 2014-2017 by The qTox Project Contributors This file is part of qTox, a Qt-based graphical interface for Tox. @@ -18,73 +18,12 @@ */ #include "audio.h" -#include "src/core/core.h" -#include "src/core/coreav.h" -#include "src/persistence/settings.h" +#include "src/audio/backend/openal.h" #include -#include -#include -#include -#include -#include -#include #include -/** - * @class Audio::Private - * - * @brief Encapsulates private audio framework from public qTox Audio API. - */ -class Audio::Private -{ -public: - Private() - : minInputGain{-30.0} - , maxInputGain{30.0} - , gain{0.0} - , gainFactor{0.0} - { - } - - static const ALchar* inDeviceNames() - { - return alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER); - } - - static const ALchar* outDeviceNames() - { - return (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE) - ? alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER) - : alcGetString(NULL, ALC_DEVICE_SPECIFIER); - } - - qreal inputGain() const - { - return gain; - } - - qreal inputGainFactor() const - { - return gainFactor; - } - - void setInputGain(qreal dB) - { - gain = qBound(minInputGain, dB, maxInputGain); - gainFactor = qPow(10.0, (gain / 20.0)); - } - -public: - qreal minInputGain; - qreal maxInputGain; - -private: - qreal gain; - qreal gainFactor; -}; - /** * @class Audio * @@ -120,605 +59,114 @@ private: * @var Audio::AUDIO_CHANNELS * @brief Ideally, we'd auto-detect, but that's a sane default * - * @var BUFFER_COUNT - * @brief Number of buffers to use per audio source + * @fn qreal Audio::outputVolume() const + * @brief Returns the current output volume (between 0 and 1) + * + * @fn void Audio::setOutputVolume(qreal volume) + * @brief Set the master output volume. + * + * @param[in] volume the master volume (between 0 and 1) + * + * @fn qreal Audio::minInputGain() const + * @brief The minimum gain value for an input device. + * + * @return minimum gain value in dB + * + * @fn void Audio::setMinInputGain(qreal dB) + * @brief Set the minimum allowed gain value in dB. + * + * @note Default is -30dB; usually you don't need to alter this value; + * + * @fn qreal Audio::maxInputGain() const + * @brief The maximum gain value for an input device. + * + * @return maximum gain value in dB + * + * @fn void Audio::setMaxInputGain(qreal dB) + * @brief Set the maximum allowed gain value in dB. + * + * @note Default is 30dB; usually you don't need to alter this value. + * + * @fn void Audio::subscribeInput() + * @brief Subscribe to capture sound from the opened input device. + * + * If the input device is not open, it will be opened before capturing. + * + * @fn void Audio::unsubscribeInput() + * @brief Unsubscribe from capturing from an opened input device. + * + * If the input device has no more subscriptions, it will be closed. + * + * @fn void Audio::playMono16Sound(const QString& path) + * @brief Play a 44100Hz mono 16bit PCM sound from a file + * + * @param[in] path the path to the sound file + * + * @fn void Audio::playMono16Sound(const QByteArray& data) + * @brief Play a 44100Hz mono 16bit PCM sound + * + * @param[in] data 44100Hz mono 16bit PCM data in host byte order + * + * @fn void Audio::playAudioBuffer(uint sourceId, const int16_t* data, int samples, + * unsigned channels, int sampleRate) + * @brief adds a number of audio frames to the play buffer + * + * @param[in] sourceId id obtained by subscribeOutput(uint &) + * @param[in] data 16bit mono or stereo PCM data with alternating channel + * mapping for stereo (LRLR) + * @param[in] samples number of samples per channel + * @param[in] channels number of channels, currently 1 or 2 is supported + * @param[in] sampleRate sample rate in Hertz + * + * @fn bool Audio::isOutputReady() const + * @brief check if the output is ready to play audio + * + * @return true if the output device is open, false otherwise + * + * @fn QStringList Audio::outDeviceNames() + * @brief Get the names of available output devices + * + * @return list of output devices + * + * @fn QStringList Audio::inDeviceNames() + * @brief Get the names of available input devices + * + * @return list of input devices + * + * @fn void Audio::subscribeOutput(uint& sid) + * @brief register a new output source + * + * param[out] sid contains the sourceId if source creation was successful, + * unchanged otherwise + * + * @fn void Audio::unsubscribeOutput(uint& sid) + * @brief unregisters an output source + * + * param[out] sid contains 0 if source deletion was successful, + * unchanged otherwise + * + * @fn void Audio::startLoop() + * @brief starts looping the sound played with playMono16Sound() + * + * @fn void Audio::stopLoop() + * @brief stops looping the sound played with playMono16Sound() + * + * @fn qreal Audio::inputGain() const + * @brief get the current input gain + * + * @return current input gain in dB + * + * @fn void Audio::setInputGain(qreal dB) + * @brief set the input gain + * + * @param[in] dB the new input gain in dB */ -static const unsigned int BUFFER_COUNT = 16; - /** * @brief Returns the singleton instance. */ Audio& Audio::getInstance() { - static Audio instance; + static OpenAL instance; return instance; } - -Audio::Audio() - : d{new Private} - , audioThread{new QThread} - , alInDev{nullptr} - , inSubscriptions{0} - , alOutDev{nullptr} - , alOutContext{nullptr} - , alMainSource{0} - , alMainBuffer{0} - , outputInitialized{false} -{ - // initialize OpenAL error stack - alGetError(); - alcGetError(nullptr); - - audioThread->setObjectName("qTox Audio"); - QObject::connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); - - moveToThread(audioThread); - - connect(&captureTimer, &QTimer::timeout, this, &Audio::doCapture); - captureTimer.setInterval(AUDIO_FRAME_DURATION / 2); - captureTimer.setSingleShot(false); - captureTimer.start(); - connect(&playMono16Timer, &QTimer::timeout, this, &Audio::playMono16SoundCleanup); - playMono16Timer.setSingleShot(true); - - audioThread->start(); -} - -Audio::~Audio() -{ - audioThread->exit(); - audioThread->wait(); - cleanupInput(); - cleanupOutput(); - delete d; -} - -void Audio::checkAlError() noexcept -{ - const ALenum al_err = alGetError(); - if (al_err != AL_NO_ERROR) - qWarning("OpenAL error: %d", al_err); -} - -void Audio::checkAlcError(ALCdevice* device) noexcept -{ - const ALCenum alc_err = alcGetError(device); - if (alc_err) - qWarning("OpenAL error: %d", alc_err); -} - -/** - * @brief Returns the current output volume (between 0 and 1) - */ -qreal Audio::outputVolume() const -{ - QMutexLocker locker(&audioLock); - - ALfloat volume = 0.0; - - if (alOutDev) { - alGetListenerf(AL_GAIN, &volume); - checkAlError(); - } - - return volume; -} - -/** - * @brief Set the master output volume. - * - * @param[in] volume the master volume (between 0 and 1) - */ -void Audio::setOutputVolume(qreal volume) -{ - QMutexLocker locker(&audioLock); - - volume = std::max(0.0, std::min(volume, 1.0)); - - alListenerf(AL_GAIN, static_cast(volume)); - checkAlError(); -} - -/** - * @brief The minimum gain value for an input device. - * - * @return minimum gain value in dB - */ -qreal Audio::minInputGain() const -{ - QMutexLocker locker(&audioLock); - return d->minInputGain; -} - -/** - * @brief Set the minimum allowed gain value in dB. - * - * @note Default is -30dB; usually you don't need to alter this value; - */ -void Audio::setMinInputGain(qreal dB) -{ - QMutexLocker locker(&audioLock); - d->minInputGain = dB; -} - -/** - * @brief The maximum gain value for an input device. - * - * @return maximum gain value in dB - */ -qreal Audio::maxInputGain() const -{ - QMutexLocker locker(&audioLock); - return d->maxInputGain; -} - -/** - * @brief Set the maximum allowed gain value in dB. - * - * @note Default is 30dB; usually you don't need to alter this value. - */ -void Audio::setMaxInputGain(qreal dB) -{ - QMutexLocker locker(&audioLock); - d->maxInputGain = dB; -} - -/** - * @brief The dB gain value. - * - * @return the gain value in dB - */ -qreal Audio::inputGain() const -{ - QMutexLocker locker(&audioLock); - return d->inputGain(); -} - -/** - * @brief Set the input gain dB level. - */ -void Audio::setInputGain(qreal dB) -{ - QMutexLocker locker(&audioLock); - d->setInputGain(dB); -} - -void Audio::reinitInput(const QString& inDevDesc) -{ - QMutexLocker locker(&audioLock); - cleanupInput(); - initInput(inDevDesc); -} - -bool Audio::reinitOutput(const QString& outDevDesc) -{ - QMutexLocker locker(&audioLock); - cleanupOutput(); - return initOutput(outDevDesc); -} - -/** - * @brief Subscribe to capture sound from the opened input device. - * - * If the input device is not open, it will be opened before capturing. - */ -void Audio::subscribeInput() -{ - QMutexLocker locker(&audioLock); - - if (!autoInitInput()) { - qWarning("Failed to subscribe to audio input device."); - return; - } - - ++inSubscriptions; - qDebug() << "Subscribed to audio input device [" << inSubscriptions << "subscriptions ]"; -} - -/** - * @brief Unsubscribe from capturing from an opened input device. - * - * If the input device has no more subscriptions, it will be closed. - */ -void Audio::unsubscribeInput() -{ - QMutexLocker locker(&audioLock); - - if (!inSubscriptions) - return; - - inSubscriptions--; - qDebug() << "Unsubscribed from audio input device [" << inSubscriptions - << "subscriptions left ]"; - - if (!inSubscriptions) - cleanupInput(); -} - -/** - * @brief Initialize audio input device, if not initialized. - * - * @return true, if device was initialized; false otherwise - */ -bool Audio::autoInitInput() -{ - return alInDev ? true : initInput(Settings::getInstance().getInDev()); -} - -/** - * @brief Initialize audio output device, if not initialized. - * - * @return true, if device was initialized; false otherwise - */ -bool Audio::autoInitOutput() -{ - return alOutDev ? true : initOutput(Settings::getInstance().getOutDev()); -} - -bool Audio::initInput(const QString& deviceName) -{ - if (!Settings::getInstance().getAudioInDevEnabled()) - return false; - - qDebug() << "Opening audio input" << deviceName; - assert(!alInDev); - - // TODO: Try to actually detect if our audio source is stereo - int stereoFlag = AUDIO_CHANNELS == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; - const uint32_t sampleRate = AUDIO_SAMPLE_RATE; - const uint16_t frameDuration = AUDIO_FRAME_DURATION; - const uint32_t chnls = AUDIO_CHANNELS; - const ALCsizei bufSize = (frameDuration * sampleRate * 4) / 1000 * chnls; - - const QByteArray qDevName = deviceName.toUtf8(); - const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData(); - alInDev = alcCaptureOpenDevice(tmpDevName, sampleRate, stereoFlag, bufSize); - - // Restart the capture if necessary - if (!alInDev) { - qWarning() << "Failed to initialize audio input device:" << deviceName; - return false; - } - - d->setInputGain(Settings::getInstance().getAudioInGainDecibel()); - - qDebug() << "Opened audio input" << deviceName; - alcCaptureStart(alInDev); - - return true; -} - -/** - * @brief Open an audio output device - */ -bool Audio::initOutput(const QString& deviceName) -{ - outSources.clear(); - - outputInitialized = false; - if (!Settings::getInstance().getAudioOutDevEnabled()) - return false; - - qDebug() << "Opening audio output" << deviceName; - assert(!alOutDev); - - const QByteArray qDevName = deviceName.toUtf8(); - const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData(); - alOutDev = alcOpenDevice(tmpDevName); - - if (!alOutDev) { - qWarning() << "Cannot open output audio device" << deviceName; - return false; - } - - qDebug() << "Opened audio output" << deviceName; - alOutContext = alcCreateContext(alOutDev, nullptr); - checkAlcError(alOutDev); - - if (!alcMakeContextCurrent(alOutContext)) { - qWarning() << "Cannot create output audio context"; - 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 44100Hz mono 16bit PCM sound from a file - */ -void Audio::playMono16Sound(const QString& path) -{ - QFile sndFile(path); - sndFile.open(QIODevice::ReadOnly); - playMono16Sound(QByteArray(sndFile.readAll())); -} - -/** - * @brief Play a 44100Hz mono 16bit PCM sound - */ -void Audio::playMono16Sound(const QByteArray& data) -{ - QMutexLocker locker(&audioLock); - - if (!autoInitOutput()) - return; - - 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); - } - - alBufferData(alMainBuffer, AL_FORMAT_MONO16, data.constData(), data.size(), 44100); - alSourcei(alMainSource, AL_BUFFER, static_cast(alMainBuffer)); - alSourcePlay(alMainSource); - - int durationMs = data.size() * 1000 / 2 / 44100; - playMono16Timer.start(durationMs + 50); -} - -void Audio::playAudioBuffer(ALuint alSource, const int16_t* data, int samples, unsigned channels, - int sampleRate) -{ - assert(channels == 1 || channels == 2); - QMutexLocker locker(&audioLock); - - if (!(alOutDev && outputInitialized)) - return; - - ALuint bufids[BUFFER_COUNT]; - ALint processed = 0, queued = 0; - alGetSourcei(alSource, AL_BUFFERS_PROCESSED, &processed); - alGetSourcei(alSource, AL_BUFFERS_QUEUED, &queued); - alSourcei(alSource, AL_LOOPING, AL_FALSE); - - if (processed == 0) { - if (queued >= BUFFER_COUNT) { - // reached limit, drop audio - return; - } - // create new buffer if none got free and we're below the limit - alGenBuffers(1, bufids); - } else { - // unqueue all processed buffers - alSourceUnqueueBuffers(alSource, processed, bufids); - // delete all but the first buffer, reuse first for new data - alDeleteBuffers(processed - 1, bufids + 1); - } - - alBufferData(bufids[0], (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data, - samples * 2 * channels, sampleRate); - alSourceQueueBuffers(alSource, 1, bufids); - - ALint state; - alGetSourcei(alSource, AL_SOURCE_STATE, &state); - if (state != AL_PLAYING) { - alSourcePlay(alSource); - } -} - -/** - * @brief Close active audio input device. - */ -void Audio::cleanupInput() -{ - if (!alInDev) - return; - - qDebug() << "Closing audio input"; - alcCaptureStop(alInDev); - if (alcCaptureCloseDevice(alInDev) == ALC_TRUE) - alInDev = nullptr; - else - qWarning() << "Failed to close input"; -} - -/** - * @brief Close active audio output device - */ -void Audio::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."); - - alcDestroyContext(alOutContext); - alOutContext = nullptr; - - qDebug() << "Closing audio output"; - if (alcCloseDevice(alOutDev)) - alOutDev = nullptr; - else - qWarning("Failed to close output."); - } -} - -/** - * @brief Called after a mono16 sound stopped playing - */ -void Audio::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; - } -} - -/** - * @brief Called on the captureTimer events to capture audio - */ -void Audio::doCapture() -{ - QMutexLocker lock(&audioLock); - - if (!alInDev || !inSubscriptions) - return; - - ALint curSamples = 0; - alcGetIntegerv(alInDev, ALC_CAPTURE_SAMPLES, sizeof(curSamples), &curSamples); - if (curSamples < AUDIO_FRAME_SAMPLE_COUNT) - return; - - int16_t buf[AUDIO_FRAME_SAMPLE_COUNT * AUDIO_CHANNELS]; - alcCaptureSamples(alInDev, buf, AUDIO_FRAME_SAMPLE_COUNT); - - for (quint32 i = 0; i < AUDIO_FRAME_SAMPLE_COUNT * AUDIO_CHANNELS; ++i) { - // gain amplification with clipping to 16-bit boundaries - int ampPCM = - qBound(std::numeric_limits::min(), qRound(buf[i] * d->inputGainFactor()), - std::numeric_limits::max()); - - buf[i] = static_cast(ampPCM); - } - - emit frameAvailable(buf, AUDIO_FRAME_SAMPLE_COUNT, AUDIO_CHANNELS, AUDIO_SAMPLE_RATE); -} - -/** - * @brief Returns true if the output device is open - */ -bool Audio::isOutputReady() const -{ - QMutexLocker locker(&audioLock); - return alOutDev && outputInitialized; -} - -QStringList Audio::outDeviceNames() -{ - QStringList list; - const ALchar* pDeviceList = Private::outDeviceNames(); - - if (pDeviceList) { - while (*pDeviceList) { - int len = static_cast(strlen(pDeviceList)); - list << QString::fromUtf8(pDeviceList, len); - pDeviceList += len + 1; - } - } - - return list; -} - -QStringList Audio::inDeviceNames() -{ - QStringList list; - const ALchar* pDeviceList = Private::inDeviceNames(); - - if (pDeviceList) { - while (*pDeviceList) { - int len = static_cast(strlen(pDeviceList)); - list << QString::fromUtf8(pDeviceList, len); - pDeviceList += len + 1; - } - } - - return list; -} - -void Audio::subscribeOutput(ALuint& sid) -{ - QMutexLocker locker(&audioLock); - - if (!autoInitOutput()) { - qWarning("Failed to subscribe to audio output device."); - return; - } - - if (!alcMakeContextCurrent(alOutContext)) { - qWarning("Failed to activate output context."); - return; - } - - alGenSources(1, &sid); - assert(sid); - outSources << sid; - - qDebug() << "Audio source" << sid << "created. Sources active:" << outSources.size(); -} - -void Audio::unsubscribeOutput(ALuint& sid) -{ - QMutexLocker locker(&audioLock); - - outSources.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:" << outSources.size(); - } else { - qWarning() << "Trying to delete invalid audio source" << sid; - } - - sid = 0; - } - - if (outSources.isEmpty()) - cleanupOutput(); -} - -void Audio::startLoop() -{ - QMutexLocker locker(&audioLock); - alSourcei(alMainSource, AL_LOOPING, AL_TRUE); -} - -void Audio::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; - } -} diff --git a/src/audio/audio.h b/src/audio/audio.h index dd9ab8b9e..950bf03b3 100644 --- a/src/audio/audio.h +++ b/src/audio/audio.h @@ -1,5 +1,5 @@ /* - Copyright © 2014-2015 by The qTox Project Contributors + Copyright © 2014-2017 by The qTox Project Contributors This file is part of qTox, a Qt-based graphical interface for Tox. @@ -30,26 +30,10 @@ #include -#if defined(__APPLE__) && defined(__MACH__) -#include -#include -#else -#include -#include -#endif - - -#ifndef ALC_ALL_DEVICES_SPECIFIER -// compatibility with older versions of OpenAL -#include -#endif - class Audio : public QObject { Q_OBJECT - class Private; - public: enum class Sound { @@ -73,85 +57,50 @@ public: } static Audio& getInstance(); - qreal outputVolume() const; - void setOutputVolume(qreal volume); + virtual qreal outputVolume() const = 0; + virtual void setOutputVolume(qreal volume) = 0; - qreal minInputGain() const; - void setMinInputGain(qreal dB); + virtual qreal minInputGain() const = 0; + virtual void setMinInputGain(qreal dB) = 0; - qreal maxInputGain() const; - void setMaxInputGain(qreal dB); + virtual qreal maxInputGain() const = 0; + virtual void setMaxInputGain(qreal dB) = 0; - qreal inputGain() const; - void setInputGain(qreal dB); + virtual qreal inputGain() const = 0; + virtual void setInputGain(qreal dB) = 0; - void reinitInput(const QString& inDevDesc); - bool reinitOutput(const QString& outDevDesc); + virtual void reinitInput(const QString& inDevDesc) = 0; + virtual bool reinitOutput(const QString& outDevDesc) = 0; - bool isOutputReady() const; + virtual bool isOutputReady() const = 0; - static QStringList outDeviceNames(); - static QStringList inDeviceNames(); + virtual QStringList outDeviceNames() = 0; + virtual QStringList inDeviceNames() = 0; - void subscribeOutput(ALuint& sid); - void unsubscribeOutput(ALuint& sid); + virtual void subscribeOutput(uint& sourceId) = 0; + virtual void unsubscribeOutput(uint& sourceId) = 0; - void subscribeInput(); - void unsubscribeInput(); + virtual void subscribeInput() = 0; + virtual void unsubscribeInput() = 0; - void startLoop(); - void stopLoop(); - void playMono16Sound(const QByteArray& data); - void playMono16Sound(const QString& path); + virtual void startLoop() = 0; + virtual void stopLoop() = 0; + virtual void playMono16Sound(const QByteArray& data) = 0; + virtual void playMono16Sound(const QString& path) = 0; - void playAudioBuffer(ALuint alSource, const int16_t* data, int samples, unsigned channels, - int sampleRate); + virtual void playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels, + int sampleRate) = 0; public: // Public default audio settings static constexpr uint32_t AUDIO_SAMPLE_RATE = 48000; static constexpr uint32_t AUDIO_FRAME_DURATION = 20; - static constexpr ALint AUDIO_FRAME_SAMPLE_COUNT = AUDIO_FRAME_DURATION * AUDIO_SAMPLE_RATE / 1000; + static constexpr uint32_t AUDIO_FRAME_SAMPLE_COUNT = AUDIO_FRAME_DURATION * AUDIO_SAMPLE_RATE / 1000; static constexpr uint32_t AUDIO_CHANNELS = 2; signals: void frameAvailable(const int16_t* pcm, size_t sample_count, uint8_t channels, uint32_t sampling_rate); - -private: - Audio(); - ~Audio(); - - static void checkAlError() noexcept; - static void checkAlcError(ALCdevice* device) noexcept; - - bool autoInitInput(); - bool autoInitOutput(); - bool initInput(const QString& deviceName); - bool initOutput(const QString& outDevDescr); - void cleanupInput(); - void cleanupOutput(); - void playMono16SoundCleanup(); - void doCapture(); - -private: - Private* d; - -private: - QThread* audioThread; - mutable QMutex audioLock; - - ALCdevice* alInDev; - quint32 inSubscriptions; - QTimer captureTimer, playMono16Timer; - - ALCdevice* alOutDev; - ALCcontext* alOutContext; - ALuint alMainSource; - ALuint alMainBuffer; - bool outputInitialized; - - QList outSources; }; #endif // AUDIO_H diff --git a/src/audio/backend/openal.cpp b/src/audio/backend/openal.cpp new file mode 100644 index 000000000..00dfb3d13 --- /dev/null +++ b/src/audio/backend/openal.cpp @@ -0,0 +1,634 @@ +/* + Copyright © 2014-2017 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + +#include "openal.h" +#include "src/core/core.h" +#include "src/core/coreav.h" +#include "src/persistence/settings.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +/** + * @class OpenAL + * @brief Provides the OpenAL audio backend + * + * @var BUFFER_COUNT + * @brief Number of buffers to use per audio source + */ + +static const unsigned int BUFFER_COUNT = 16; + +OpenAL::OpenAL() : + audioThread{new QThread} + , alInDev{nullptr} + , inSubscriptions{0} + , alOutDev{nullptr} + , alOutContext{nullptr} + , alMainSource{0} + , alMainBuffer{0} + , outputInitialized{false} +{ + // initialize OpenAL error stack + alGetError(); + alcGetError(nullptr); + + audioThread->setObjectName("qTox Audio"); + QObject::connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); + + moveToThread(audioThread); + + connect(&captureTimer, &QTimer::timeout, this, &OpenAL::doCapture); + captureTimer.setInterval(AUDIO_FRAME_DURATION / 2); + captureTimer.setSingleShot(false); + captureTimer.start(); + connect(&playMono16Timer, &QTimer::timeout, this, &OpenAL::playMono16SoundCleanup); + playMono16Timer.setSingleShot(true); + + audioThread->start(); +} + +OpenAL::~OpenAL() +{ + audioThread->exit(); + audioThread->wait(); + cleanupInput(); + cleanupOutput(); +} + +void OpenAL::checkAlError() noexcept +{ + const ALenum al_err = alGetError(); + if (al_err != AL_NO_ERROR) + qWarning("OpenAL error: %d", al_err); +} + +void OpenAL::checkAlcError(ALCdevice* device) noexcept +{ + const ALCenum alc_err = alcGetError(device); + if (alc_err) + qWarning("OpenAL error: %d", alc_err); +} + +/** + * @brief Returns the current output volume (between 0 and 1) + */ +qreal OpenAL::outputVolume() const +{ + QMutexLocker locker(&audioLock); + + ALfloat volume = 0.0; + + if (alOutDev) { + alGetListenerf(AL_GAIN, &volume); + checkAlError(); + } + + return volume; +} + +/** + * @brief Set the master output volume. + * + * @param[in] volume the master volume (between 0 and 1) + */ +void OpenAL::setOutputVolume(qreal volume) +{ + QMutexLocker locker(&audioLock); + + volume = std::max(0.0, std::min(volume, 1.0)); + + alListenerf(AL_GAIN, static_cast(volume)); + checkAlError(); +} + +/** + * @brief The minimum gain value for an input device. + * + * @return minimum gain value in dB + */ +qreal OpenAL::minInputGain() const +{ + QMutexLocker locker(&audioLock); + return minInGain; +} + +/** + * @brief Set the minimum allowed gain value in dB. + * + * @note Default is -30dB; usually you don't need to alter this value; + */ +void OpenAL::setMinInputGain(qreal dB) +{ + QMutexLocker locker(&audioLock); + minInGain = dB; +} + +/** + * @brief The maximum gain value for an input device. + * + * @return maximum gain value in dB + */ +qreal OpenAL::maxInputGain() const +{ + QMutexLocker locker(&audioLock); + return maxInGain; +} + +/** + * @brief Set the maximum allowed gain value in dB. + * + * @note Default is 30dB; usually you don't need to alter this value. + */ +void OpenAL::setMaxInputGain(qreal dB) +{ + QMutexLocker locker(&audioLock); + maxInGain = dB; +} + +void OpenAL::reinitInput(const QString& inDevDesc) +{ + QMutexLocker locker(&audioLock); + cleanupInput(); + initInput(inDevDesc); +} + +bool OpenAL::reinitOutput(const QString& outDevDesc) +{ + QMutexLocker locker(&audioLock); + cleanupOutput(); + return initOutput(outDevDesc); +} + +/** + * @brief Subscribe to capture sound from the opened input device. + * + * If the input device is not open, it will be opened before capturing. + */ +void OpenAL::subscribeInput() +{ + QMutexLocker locker(&audioLock); + + if (!autoInitInput()) { + qWarning("Failed to subscribe to audio input device."); + return; + } + + ++inSubscriptions; + qDebug() << "Subscribed to audio input device [" << inSubscriptions << "subscriptions ]"; +} + +/** + * @brief Unsubscribe from capturing from an opened input device. + * + * If the input device has no more subscriptions, it will be closed. + */ +void OpenAL::unsubscribeInput() +{ + QMutexLocker locker(&audioLock); + + if (!inSubscriptions) + return; + + inSubscriptions--; + qDebug() << "Unsubscribed from audio input device [" << inSubscriptions + << "subscriptions left ]"; + + if (!inSubscriptions) + cleanupInput(); +} + +/** + * @brief Initialize audio input device, if not initialized. + * + * @return true, if device was initialized; false otherwise + */ +bool OpenAL::autoInitInput() +{ + return alInDev ? true : initInput(Settings::getInstance().getInDev()); +} + +/** + * @brief Initialize audio output device, if not initialized. + * + * @return true, if device was initialized; false otherwise + */ +bool OpenAL::autoInitOutput() +{ + return alOutDev ? true : initOutput(Settings::getInstance().getOutDev()); +} + +bool OpenAL::initInput(const QString& deviceName) +{ + if (!Settings::getInstance().getAudioInDevEnabled()) + return false; + + qDebug() << "Opening audio input" << deviceName; + assert(!alInDev); + + // TODO: Try to actually detect if our audio source is stereo + int stereoFlag = AUDIO_CHANNELS == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; + const uint32_t sampleRate = AUDIO_SAMPLE_RATE; + const uint16_t frameDuration = AUDIO_FRAME_DURATION; + const uint32_t chnls = AUDIO_CHANNELS; + const ALCsizei bufSize = (frameDuration * sampleRate * 4) / 1000 * chnls; + + const QByteArray qDevName = deviceName.toUtf8(); + const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData(); + alInDev = alcCaptureOpenDevice(tmpDevName, sampleRate, stereoFlag, bufSize); + + // Restart the capture if necessary + if (!alInDev) { + qWarning() << "Failed to initialize audio input device:" << deviceName; + return false; + } + + setInputGain(Settings::getInstance().getAudioInGainDecibel()); + + qDebug() << "Opened audio input" << deviceName; + alcCaptureStart(alInDev); + + return true; +} + +/** + * @brief Open an audio output device + */ +bool OpenAL::initOutput(const QString& deviceName) +{ + outSources.clear(); + + outputInitialized = false; + if (!Settings::getInstance().getAudioOutDevEnabled()) + return false; + + qDebug() << "Opening audio output" << deviceName; + assert(!alOutDev); + + const QByteArray qDevName = deviceName.toUtf8(); + const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData(); + alOutDev = alcOpenDevice(tmpDevName); + + if (!alOutDev) { + qWarning() << "Cannot open output audio device" << deviceName; + return false; + } + + qDebug() << "Opened audio output" << deviceName; + alOutContext = alcCreateContext(alOutDev, nullptr); + checkAlcError(alOutDev); + + if (!alcMakeContextCurrent(alOutContext)) { + qWarning() << "Cannot create output audio context"; + 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 44100Hz 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 44100Hz mono 16bit PCM sound + */ +void OpenAL::playMono16Sound(const QByteArray& data) +{ + QMutexLocker locker(&audioLock); + + if (!autoInitOutput()) + return; + + 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); + } + + alBufferData(alMainBuffer, AL_FORMAT_MONO16, data.constData(), data.size(), 44100); + alSourcei(alMainSource, AL_BUFFER, static_cast(alMainBuffer)); + alSourcePlay(alMainSource); + + int durationMs = data.size() * 1000 / 2 / 44100; + playMono16Timer.start(durationMs + 50); +} + +void OpenAL::playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels, + int sampleRate) +{ + assert(channels == 1 || channels == 2); + QMutexLocker locker(&audioLock); + + if (!(alOutDev && outputInitialized)) + return; + + ALuint bufids[BUFFER_COUNT]; + ALint processed = 0, queued = 0; + alGetSourcei(sourceId, AL_BUFFERS_PROCESSED, &processed); + alGetSourcei(sourceId, AL_BUFFERS_QUEUED, &queued); + alSourcei(sourceId, AL_LOOPING, AL_FALSE); + + if (processed == 0) { + if (queued >= BUFFER_COUNT) { + // reached limit, drop audio + return; + } + // create new buffer if none got free and we're below the limit + alGenBuffers(1, bufids); + } else { + // unqueue all processed buffers + alSourceUnqueueBuffers(sourceId, processed, bufids); + // delete all but the first buffer, reuse first for new data + alDeleteBuffers(processed - 1, bufids + 1); + } + + alBufferData(bufids[0], (channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, data, + samples * 2 * channels, sampleRate); + alSourceQueueBuffers(sourceId, 1, bufids); + + ALint state; + alGetSourcei(sourceId, AL_SOURCE_STATE, &state); + if (state != AL_PLAYING) { + alSourcePlay(sourceId); + } +} + +/** + * @brief Close active audio input device. + */ +void OpenAL::cleanupInput() +{ + if (!alInDev) + return; + + qDebug() << "Closing audio input"; + alcCaptureStop(alInDev); + if (alcCaptureCloseDevice(alInDev) == ALC_TRUE) + alInDev = nullptr; + else + qWarning() << "Failed to close input"; +} + +/** + * @brief Close active audio output device + */ +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."); + + alcDestroyContext(alOutContext); + alOutContext = nullptr; + + qDebug() << "Closing audio output"; + if (alcCloseDevice(alOutDev)) + alOutDev = nullptr; + else + qWarning("Failed to close output."); + } +} + +/** + * @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; + } + else + { + // the audio didn't finish, try again later + playMono16Timer.start(10); + } +} + +/** + * @brief Called on the captureTimer events to capture audio + */ +void OpenAL::doCapture() +{ + QMutexLocker lock(&audioLock); + + if (!alInDev || !inSubscriptions) + return; + + ALint curSamples = 0; + alcGetIntegerv(alInDev, ALC_CAPTURE_SAMPLES, sizeof(curSamples), &curSamples); + if (curSamples < AUDIO_FRAME_SAMPLE_COUNT) + return; + + int16_t buf[AUDIO_FRAME_SAMPLE_COUNT * AUDIO_CHANNELS]; + alcCaptureSamples(alInDev, buf, AUDIO_FRAME_SAMPLE_COUNT); + + for (quint32 i = 0; i < AUDIO_FRAME_SAMPLE_COUNT * AUDIO_CHANNELS; ++i) { + // gain amplification with clipping to 16-bit boundaries + int ampPCM = + qBound(std::numeric_limits::min(), qRound(buf[i] * inputGainFactor()), + std::numeric_limits::max()); + + buf[i] = static_cast(ampPCM); + } + + emit Audio::frameAvailable(buf, AUDIO_FRAME_SAMPLE_COUNT, AUDIO_CHANNELS, AUDIO_SAMPLE_RATE); +} + +/** + * @brief Returns true if the output device is open + */ +bool OpenAL::isOutputReady() const +{ + QMutexLocker locker(&audioLock); + return alOutDev && outputInitialized; +} + +QStringList OpenAL::outDeviceNames() +{ + QStringList list; + const ALchar* pDeviceList = (alcIsExtensionPresent(NULL, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE) + ? alcGetString(NULL, ALC_ALL_DEVICES_SPECIFIER) + : alcGetString(NULL, ALC_DEVICE_SPECIFIER); + + if (pDeviceList) { + while (*pDeviceList) { + int len = static_cast(strlen(pDeviceList)); + list << QString::fromUtf8(pDeviceList, len); + pDeviceList += len + 1; + } + } + + return list; +} + +QStringList OpenAL::inDeviceNames() +{ + QStringList list; + const ALchar* pDeviceList = alcGetString(NULL, ALC_CAPTURE_DEVICE_SPECIFIER); + + if (pDeviceList) { + while (*pDeviceList) { + int len = static_cast(strlen(pDeviceList)); + list << QString::fromUtf8(pDeviceList, len); + pDeviceList += len + 1; + } + } + + return list; +} + +void OpenAL::subscribeOutput(uint& sid) +{ + QMutexLocker locker(&audioLock); + + if (!autoInitOutput()) { + qWarning("Failed to subscribe to audio output device."); + return; + } + + if (!alcMakeContextCurrent(alOutContext)) { + qWarning("Failed to activate output context."); + return; + } + + alGenSources(1, &sid); + assert(sid); + outSources << sid; + + qDebug() << "Audio source" << sid << "created. Sources active:" << outSources.size(); +} + +void OpenAL::unsubscribeOutput(uint& sid) +{ + QMutexLocker locker(&audioLock); + + outSources.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:" << outSources.size(); + } else { + qWarning() << "Trying to delete invalid audio source" << sid; + } + + sid = 0; + } + + if (outSources.isEmpty()) + cleanupOutput(); +} + +void OpenAL::startLoop() +{ + 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; + } +} + +qreal OpenAL::inputGain() const +{ + return gain; +} + +qreal OpenAL::inputGainFactor() const +{ + return gainFactor; +} + +void OpenAL::setInputGain(qreal dB) +{ + gain = qBound(minInGain, dB, maxInGain); + gainFactor = qPow(10.0, (gain / 20.0)); +} diff --git a/src/audio/backend/openal.h b/src/audio/backend/openal.h new file mode 100644 index 000000000..2d218b09e --- /dev/null +++ b/src/audio/backend/openal.h @@ -0,0 +1,127 @@ +/* + Copyright © 2014-2017 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with qTox. If not, see . +*/ + + +#ifndef OPENAL_H +#define OPENAL_H + +#include "src/audio/audio.h" + +#include +#include + +#include +#include +#include + +#include + +#if defined(__APPLE__) && defined(__MACH__) +#include +#include +#else +#include +#include +#endif + + +#ifndef ALC_ALL_DEVICES_SPECIFIER +// compatibility with older versions of OpenAL +#include +#endif + +class OpenAL : public Audio +{ + Q_OBJECT + +public: + OpenAL(); + ~OpenAL(); + + qreal outputVolume() const; + void setOutputVolume(qreal volume); + + qreal minInputGain() const; + void setMinInputGain(qreal dB); + + qreal maxInputGain() const; + void setMaxInputGain(qreal dB); + + qreal inputGain() const; + void setInputGain(qreal dB); + + void reinitInput(const QString& inDevDesc); + bool reinitOutput(const QString& outDevDesc); + + bool isOutputReady() const; + + QStringList outDeviceNames(); + QStringList inDeviceNames(); + + void subscribeOutput(uint& sourceId); + void unsubscribeOutput(uint& sourceId); + + void subscribeInput(); + void unsubscribeInput(); + + void startLoop(); + void stopLoop(); + void playMono16Sound(const QByteArray& data); + void playMono16Sound(const QString& path); + + void playAudioBuffer(uint sourceId, const int16_t* data, int samples, unsigned channels, + int sampleRate); + +private: + + static void checkAlError() noexcept; + static void checkAlcError(ALCdevice* device) noexcept; + + bool autoInitInput(); + bool autoInitOutput(); + bool initInput(const QString& deviceName); + bool initOutput(const QString& outDevDescr); + void cleanupInput(); + void cleanupOutput(); + void playMono16SoundCleanup(); + void doCapture(); + qreal inputGainFactor() const; + +private: + QThread* audioThread; + mutable QMutex audioLock; + + ALCdevice* alInDev; + quint32 inSubscriptions; + QTimer captureTimer, playMono16Timer; + + ALCdevice* alOutDev; + ALCcontext* alOutContext; + ALuint alMainSource; + ALuint alMainBuffer; + bool outputInitialized; + + QList outSources; + qreal gain; + qreal gainFactor; + qreal minInGain = -30; + qreal maxInGain = 30; +}; + +#endif // OPENAL_H diff --git a/src/widget/form/settings/avform.cpp b/src/widget/form/settings/avform.cpp index 98f8dd810..6404728b9 100644 --- a/src/widget/form/settings/avform.cpp +++ b/src/widget/form/settings/avform.cpp @@ -424,7 +424,7 @@ int AVForm::getModeSize(VideoMode mode) void AVForm::getAudioInDevices() { QStringList deviceNames; - deviceNames << tr("Disabled") << Audio::inDeviceNames(); + deviceNames << tr("Disabled") << Audio::getInstance().inDeviceNames(); inDevCombobox->blockSignals(true); inDevCombobox->clear(); @@ -443,7 +443,7 @@ void AVForm::getAudioInDevices() void AVForm::getAudioOutDevices() { QStringList deviceNames; - deviceNames << tr("Disabled") << Audio::outDeviceNames(); + deviceNames << tr("Disabled") << Audio::getInstance().outDeviceNames(); outDevCombobox->blockSignals(true); outDevCombobox->clear();