/* Copyright © 2014-2019 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/persistence/settings.h" #include #include #include #include #include #include #include #include namespace { void applyGain(int16_t* buffer, uint32_t bufferSize, qreal gainFactor) { for (quint32 i = 0; i < bufferSize; ++i) { // gain amplification with clipping to 16-bit boundaries buffer[i] = qBound(std::numeric_limits::min(), qRound(buffer[i] * gainFactor), std::numeric_limits::max()); } } } // namespace /** * @class OpenAL * @brief Provides the OpenAL audio backend * * @var BUFFER_COUNT * @brief Number of buffers to use per audio source * * @var AUDIO_CHANNELS * @brief Ideally, we'd auto-detect, but that's a sane default */ static const unsigned int BUFFER_COUNT = 16; static const uint32_t AUDIO_CHANNELS = 2; OpenAL::OpenAL() : audioThread{new QThread} { // initialize OpenAL error stack alGetError(); alcGetError(nullptr); 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, audioThread, &QThread::deleteLater); moveToThread(audioThread); voiceTimer.setSingleShot(true); voiceTimer.moveToThread(audioThread); connect(this, &OpenAL::startActive, &voiceTimer, static_cast(&QTimer::start)); connect(&voiceTimer, &QTimer::timeout, this, &OpenAL::stopActive); connect(&captureTimer, &QTimer::timeout, this, &OpenAL::doAudio); captureTimer.setInterval(AUDIO_FRAME_DURATION / 2); captureTimer.setSingleShot(false); captureTimer.moveToThread(audioThread); // TODO for Qt 5.6+: use qOverload connect(audioThread, &QThread::started, &captureTimer, static_cast(&QTimer::start)); 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(); } 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; } /** * @brief The minimum threshold value for an input device. * * @return minimum normalized threshold */ qreal OpenAL::minInputThreshold() const { QMutexLocker locker(&audioLock); return minInThreshold; } /** * @brief The maximum normalized threshold value for an input device. * * @return maximum normalized threshold */ qreal OpenAL::maxInputThreshold() const { QMutexLocker locker(&audioLock); return maxInThreshold; } void OpenAL::reinitInput(const QString& inDevDesc) { QMutexLocker locker(&audioLock); const auto bakSources = sources; sources.clear(); cleanupInput(); initInput(inDevDesc); locker.unlock(); // this must happen outside `audioLock`, to avoid a deadlock when // a slot on AlSource::invalidate tries to create a new source immedeately. for (auto& source : bakSources) { source->kill(); } } bool OpenAL::reinitOutput(const QString& outDevDesc) { QMutexLocker locker(&audioLock); const auto bakSinks = sinks; sinks.clear(); cleanupOutput(); 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 */ std::unique_ptr 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 std::unique_ptr{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(); } } /** * @brief Subscribe to capture sound from the opened input device. * * If the input device is not open, it will be opened before capturing. */ std::unique_ptr OpenAL::makeSource() { QMutexLocker locker(&audioLock); if (!autoInitInput()) { qWarning("Failed to subscribe to audio input device."); return {}; } auto const source = new AlSource(*this); if (source == nullptr) { return {}; } sources.insert(source); qDebug() << "Subscribed to audio input device [" << sources.size() << "subscriptions ]"; return std::unique_ptr{source}; } /** * @brief Unsubscribe from capturing from an opened input device. * * If the input device has no more subscriptions, it will be closed. */ void OpenAL::destroySource(AlSource& source) { QMutexLocker locker(&audioLock); const auto s = sources.find(&source); if (s == sources.end()) { qWarning() << "Destroyed non-existant source"; return; } sources.erase(s); qDebug() << "Unsubscribed from audio input device [" << sources.size() << "subscriptions left ]"; if (sources.empty()) { 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) { return initInput(deviceName, AUDIO_CHANNELS); } bool OpenAL::initInput(const QString& deviceName, uint32_t channels) { if (!Settings::getInstance().getAudioInDevEnabled()) { return false; } qDebug() << "Opening audio input" << deviceName; assert(!alInDev); // TODO: Try to actually detect if our audio source is stereo this->channels = channels; int stereoFlag = channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16; const int bytesPerSample = 2; const int safetyFactor = 2; // internal OpenAL ring buffer. must be larger than our inputBuffer // to avoid the ring from overwriting itself between captures. AUDIO_FRAME_SAMPLE_COUNT_TOTAL = AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL * channels; const ALCsizei ringBufSize = AUDIO_FRAME_SAMPLE_COUNT_TOTAL * bytesPerSample * safetyFactor; const QByteArray qDevName = deviceName.toUtf8(); const ALchar* tmpDevName = qDevName.isEmpty() ? nullptr : qDevName.constData(); alInDev = alcCaptureOpenDevice(tmpDevName, AUDIO_SAMPLE_RATE, stereoFlag, ringBufSize); // Restart the capture if necessary if (!alInDev) { qWarning() << "Failed to initialize audio input device:" << deviceName; return false; } inputBuffer = new int16_t[AUDIO_FRAME_SAMPLE_COUNT_TOTAL]; setInputGain(Settings::getInstance().getAudioInGainDecibel()); setInputThreshold(Settings::getInstance().getAudioThreshold()); qDebug() << "Opened audio input" << deviceName; alcCaptureStart(alInDev); return true; } /** * @brief Open an audio output device */ bool OpenAL::initOutput(const QString& deviceName) { // there should be no sinks when initializing the output assert(sinks.size() == 0); 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; } // init master volume alListenerf(AL_GAIN, Settings::getInstance().getOutVolume() * 0.01f); checkAlError(); outputInitialized = true; return true; } /** * @brief Play a 48kHz mono 16bit PCM sound */ 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); auto sinkIt = soundSinks.begin(); while (sinkIt != soundSinks.end()) { auto sink = *sinkIt; ALuint sourceId = sink->getSourceId(); ALint state = 0; alGetSourcei(sourceId, AL_SOURCE_STATE, &state); if (state != AL_PLAYING) { sinkIt = soundSinks.erase(sinkIt); emit sink->finishedPlaying(); } else { ++sinkIt; } } } 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 (static_cast(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"; } delete[] inputBuffer; } /** * @brief Close active audio output device */ void OpenAL::cleanupOutput() { outputInitialized = false; if (alOutDev) { 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 by doCapture to calculate volume of the audio buffer * * @param[in] buf the current audio buffer * * @return normalized volume between 0-1 */ float OpenAL::getVolume() { const quint32 samples = AUDIO_FRAME_SAMPLE_COUNT_TOTAL; const float rootTwo = 1.414213562; // sqrt(2), but sqrt is not constexpr // calculate volume as the root mean squared of amplitudes in the sample float sumOfSquares = 0; for (quint32 i = 0; i < samples; i++) { float sample = static_cast(inputBuffer[i]) / std::numeric_limits::max(); sumOfSquares += std::pow(sample, 2); } const float rms = std::sqrt(sumOfSquares / samples); // our calculated normalized volume could possibly be above 1 because our RMS assumes a sinusoidal wave const float normalizedVolume = std::min(rms * rootTwo, 1.0f); return normalizedVolume; } /** * @brief Called by voiceTimer's timeout to disable audio broadcasting */ void OpenAL::stopActive() { isActive = false; } /** * @brief handles recording of audio frames */ void OpenAL::doInput() { ALint curSamples = 0; alcGetIntegerv(alInDev, ALC_CAPTURE_SAMPLES, sizeof(curSamples), &curSamples); if (curSamples < static_cast(AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL)) { return; } captureSamples(alInDev, inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL); applyGain(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_TOTAL, gainFactor); float volume = getVolume(); if (volume >= inputThreshold) { isActive = true; emit startActive(voiceHold); } else if (!isActive) { volume = 0; } // NOTE(sudden6): this loop probably doesn't scale too well with many sources for (auto source : sources) { emit source->volumeAvailable(volume); } if (!isActive) { return; } // NOTE(sudden6): this loop probably doesn't scale too well with many sources for (auto source : sources) { emit source->frameAvailable(inputBuffer, AUDIO_FRAME_SAMPLE_COUNT_PER_CHANNEL, channels, AUDIO_SAMPLE_RATE); } } void OpenAL::doOutput() { // Nothing } /** * @brief Called on the captureTimer events to capture audio */ void OpenAL::doAudio() { QMutexLocker lock(&audioLock); // Output section does nothing // Input section if (alInDev && !sources.empty()) { doInput(); } } void OpenAL::captureSamples(ALCdevice* device, int16_t* buffer, ALCsizei samples) { alcCaptureSamples(device, buffer, samples); } /** * @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(nullptr, "ALC_ENUMERATE_ALL_EXT") != AL_FALSE) ? alcGetString(nullptr, ALC_ALL_DEVICES_SPECIFIER) : alcGetString(nullptr, 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(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER); if (pDeviceList) { while (*pDeviceList) { int len = static_cast(strlen(pDeviceList)); list << QString::fromUtf8(pDeviceList, len); pDeviceList += len + 1; } } return list; } /** * @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::startLoop(uint sourceId) { QMutexLocker locker(&audioLock); alSourcei(sourceId, AL_LOOPING, AL_TRUE); } void OpenAL::stopLoop(uint sourceId) { QMutexLocker locker(&audioLock); alSourcei(sourceId, AL_LOOPING, AL_FALSE); alSourceStop(sourceId); cleanupBuffers(sourceId); } qreal OpenAL::inputGain() const { return gain; } qreal OpenAL::getInputThreshold() const { return inputThreshold; } qreal OpenAL::inputGainFactor() const { return gainFactor; } void OpenAL::setInputGain(qreal dB) { gain = qBound(minInGain, dB, maxInGain); gainFactor = qPow(10.0, (gain / 20.0)); } void OpenAL::setInputThreshold(qreal normalizedThreshold) { inputThreshold = normalizedThreshold; }