mirror of https://github.com/qTox/qTox
813 lines
21 KiB
C++
813 lines
21 KiB
C++
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "openal.h"
|
|
#include "src/persistence/settings.h"
|
|
|
|
#include <QDebug>
|
|
#include <QFile>
|
|
#include <QMutexLocker>
|
|
#include <QPointer>
|
|
#include <QThread>
|
|
#include <QWaitCondition>
|
|
#include <QtMath>
|
|
|
|
#include <cassert>
|
|
|
|
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<int16_t>(std::numeric_limits<int16_t>::min(), qRound(buffer[i] * gainFactor),
|
|
std::numeric_limits<int16_t>::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<void (QTimer::*)(int)>(&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<void (QTimer::*)(void)>(&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<void (QTimer::*)(void)>(&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<ALfloat>(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<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 std::unique_ptr<IAudioSink>{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<IAudioSource> 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<IAudioSource>{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<ALuint>(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<float>(inputBuffer[i]) / std::numeric_limits<int16_t>::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<ALint>(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<int>(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<int>(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<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::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;
|
|
}
|