466 lines
12 KiB
C++
466 lines
12 KiB
C++
// Copyright 2007-2023 The Mumble Developers. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license
|
|
// that can be found in the LICENSE file at the root of the
|
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
|
|
|
#include "PAAudio.h"
|
|
#include "Global.h"
|
|
|
|
#ifdef Q_CC_GNU
|
|
# define RESOLVE(var) \
|
|
{ \
|
|
var = reinterpret_cast< __typeof__(var) >(qlPortAudio.resolve(#var)); \
|
|
if (!var) \
|
|
return; \
|
|
}
|
|
#else
|
|
# define RESOLVE(var) \
|
|
{ \
|
|
*reinterpret_cast< void ** >(&var) = static_cast< void * >(qlPortAudio.resolve(#var)); \
|
|
if (!var) \
|
|
return; \
|
|
}
|
|
#endif
|
|
|
|
static std::unique_ptr< PortAudioSystem > pas;
|
|
|
|
class PortAudioInputRegistrar : public AudioInputRegistrar {
|
|
private:
|
|
AudioInput *create() Q_DECL_OVERRIDE;
|
|
const QVariant getDeviceChoice() Q_DECL_OVERRIDE;
|
|
const QList< audioDevice > getDeviceChoices() Q_DECL_OVERRIDE;
|
|
void setDeviceChoice(const QVariant &, Settings &) Q_DECL_OVERRIDE;
|
|
bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const Q_DECL_OVERRIDE;
|
|
bool isMicrophoneAccessDeniedByOS() Q_DECL_OVERRIDE { return false; };
|
|
|
|
public:
|
|
PortAudioInputRegistrar();
|
|
};
|
|
|
|
class PortAudioOutputRegistrar : public AudioOutputRegistrar {
|
|
private:
|
|
AudioOutput *create() Q_DECL_OVERRIDE;
|
|
const QVariant getDeviceChoice() Q_DECL_OVERRIDE;
|
|
const QList< audioDevice > getDeviceChoices() Q_DECL_OVERRIDE;
|
|
void setDeviceChoice(const QVariant &, Settings &) Q_DECL_OVERRIDE;
|
|
|
|
public:
|
|
PortAudioOutputRegistrar();
|
|
};
|
|
|
|
class PortAudioInit : public DeferInit {
|
|
private:
|
|
std::unique_ptr< PortAudioInputRegistrar > airPortAudio;
|
|
std::unique_ptr< PortAudioOutputRegistrar > aorPortAudio;
|
|
void initialize();
|
|
void destroy();
|
|
};
|
|
|
|
PortAudioInputRegistrar::PortAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("PortAudio")) {
|
|
}
|
|
|
|
AudioInput *PortAudioInputRegistrar::create() {
|
|
return new PortAudioInput();
|
|
}
|
|
|
|
const QVariant PortAudioInputRegistrar::getDeviceChoice() {
|
|
return Global::get().s.iPortAudioInput;
|
|
}
|
|
|
|
const QList< audioDevice > PortAudioInputRegistrar::getDeviceChoices() {
|
|
return pas->enumerateDevices(true);
|
|
}
|
|
|
|
void PortAudioInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
|
|
s.iPortAudioInput = choice.toInt();
|
|
}
|
|
|
|
bool PortAudioInputRegistrar::canEcho(EchoCancelOptionID, const QString &) const {
|
|
return false;
|
|
}
|
|
|
|
PortAudioOutputRegistrar::PortAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("PortAudio")) {
|
|
}
|
|
|
|
AudioOutput *PortAudioOutputRegistrar::create() {
|
|
return new PortAudioOutput();
|
|
}
|
|
|
|
const QVariant PortAudioOutputRegistrar::getDeviceChoice() {
|
|
return Global::get().s.iPortAudioOutput;
|
|
}
|
|
|
|
const QList< audioDevice > PortAudioOutputRegistrar::getDeviceChoices() {
|
|
return pas->enumerateDevices(false);
|
|
}
|
|
|
|
void PortAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
|
|
s.iPortAudioOutput = choice.toInt();
|
|
}
|
|
|
|
void PortAudioInit::initialize() {
|
|
pas.reset(new PortAudioSystem());
|
|
|
|
if (pas->bOk) {
|
|
airPortAudio.reset(new PortAudioInputRegistrar());
|
|
aorPortAudio.reset(new PortAudioOutputRegistrar());
|
|
} else {
|
|
pas.reset();
|
|
}
|
|
}
|
|
|
|
void PortAudioInit::destroy() {
|
|
airPortAudio.reset();
|
|
aorPortAudio.reset();
|
|
pas.reset();
|
|
}
|
|
|
|
// Instantiate PortAudioSystem, PortAudioInputRegistrar and PortAudioOutputRegistrar
|
|
static PortAudioInit pai;
|
|
|
|
PortAudioSystem::PortAudioSystem() : bOk(false) {
|
|
QStringList alternatives;
|
|
#ifdef Q_OS_WIN
|
|
alternatives << QLatin1String("portaudio_x64.dll");
|
|
alternatives << QLatin1String("portaudio_x86.dll");
|
|
#elif defined(Q_OS_MAC)
|
|
alternatives << QLatin1String("libportaudio.dylib");
|
|
alternatives << QLatin1String("libportaudio.2.dylib");
|
|
#else
|
|
alternatives << QLatin1String("libportaudio.so");
|
|
alternatives << QLatin1String("libportaudio.so.2");
|
|
#endif
|
|
for (const auto &lib : alternatives) {
|
|
qlPortAudio.setFileName(lib);
|
|
if (qlPortAudio.load()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!qlPortAudio.isLoaded()) {
|
|
return;
|
|
}
|
|
|
|
RESOLVE(Pa_GetVersionText)
|
|
RESOLVE(Pa_GetErrorText)
|
|
RESOLVE(Pa_Initialize)
|
|
RESOLVE(Pa_Terminate)
|
|
RESOLVE(Pa_OpenStream)
|
|
RESOLVE(Pa_CloseStream)
|
|
RESOLVE(Pa_StartStream)
|
|
RESOLVE(Pa_StopStream)
|
|
RESOLVE(Pa_IsStreamActive)
|
|
RESOLVE(Pa_GetDefaultInputDevice)
|
|
RESOLVE(Pa_GetDefaultOutputDevice)
|
|
RESOLVE(Pa_HostApiDeviceIndexToDeviceIndex)
|
|
RESOLVE(Pa_GetHostApiCount)
|
|
RESOLVE(Pa_GetHostApiInfo)
|
|
RESOLVE(Pa_GetDeviceInfo)
|
|
|
|
const auto ret = Pa_Initialize();
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to initialize library - Pa_Initialize() returned: %s", Pa_GetErrorText(ret));
|
|
return;
|
|
}
|
|
|
|
bOk = true;
|
|
|
|
qDebug("%s from %s", Pa_GetVersionText(), qPrintable(qlPortAudio.fileName()));
|
|
}
|
|
|
|
PortAudioSystem::~PortAudioSystem() {
|
|
if (bOk) {
|
|
const auto ret = Pa_Terminate();
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to terminate library - Pa_Terminate() returned: %s",
|
|
Pa_GetErrorText(ret));
|
|
}
|
|
}
|
|
}
|
|
|
|
const QList< audioDevice > PortAudioSystem::enumerateDevices(const bool input) {
|
|
QList< audioDevice > audioDevices;
|
|
|
|
if (!bOk) {
|
|
return audioDevices;
|
|
}
|
|
|
|
audioDevices << audioDevice(tr("Default device"), -1);
|
|
|
|
for (PaHostApiIndex apiIndex = 0; apiIndex < Pa_GetHostApiCount(); ++apiIndex) {
|
|
const auto *apiInfo = Pa_GetHostApiInfo(apiIndex);
|
|
if (!apiInfo) {
|
|
continue;
|
|
}
|
|
|
|
for (PaDeviceIndex apiDeviceIndex = 0; apiDeviceIndex < apiInfo->deviceCount; ++apiDeviceIndex) {
|
|
const auto deviceIndex = Pa_HostApiDeviceIndexToDeviceIndex(apiIndex, apiDeviceIndex);
|
|
const auto *deviceInfo = Pa_GetDeviceInfo(deviceIndex);
|
|
if (!deviceInfo) {
|
|
continue;
|
|
}
|
|
|
|
if ((input && (deviceInfo->maxInputChannels > 0)) || (!input && (deviceInfo->maxOutputChannels > 0))) {
|
|
audioDevices << audioDevice(
|
|
QLatin1String(apiInfo->name) + QLatin1String(": ") + QLatin1String(deviceInfo->name), deviceIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
return audioDevices;
|
|
}
|
|
|
|
bool PortAudioSystem::isStreamRunning(PaStream *stream) {
|
|
if (!bOk || !stream) {
|
|
return false;
|
|
}
|
|
|
|
const auto ret = Pa_IsStreamActive(stream);
|
|
if (ret == 1) {
|
|
return true;
|
|
} else if (ret != 0) {
|
|
qWarning("PortAudioSystem: failed to determine stream status - Pa_IsStreamActive() returned: %s",
|
|
Pa_GetErrorText(ret));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int PortAudioSystem::openStream(PaStream **stream, PaDeviceIndex device, const uint32_t frameSize, const bool isInput) {
|
|
if (!bOk || !stream) {
|
|
return 0;
|
|
}
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
// -1 is the default device
|
|
if (device == -1) {
|
|
device = (isInput ? Pa_GetDefaultInputDevice() : Pa_GetDefaultOutputDevice());
|
|
}
|
|
|
|
const auto *devInfo = Pa_GetDeviceInfo(device);
|
|
if (!devInfo) {
|
|
qWarning("PortAudioSystem: failed to retrieve info about device %d - Pa_GetDeviceInfo() returned: nullptr",
|
|
device);
|
|
return 0;
|
|
}
|
|
|
|
const auto *apiInfo = Pa_GetHostApiInfo(devInfo->hostApi);
|
|
if (!apiInfo) {
|
|
qWarning(
|
|
"PortAudioSystem: failed to retrieve info about API %d (device %d) - Pa_GetHostApiInfo() returned: nullptr",
|
|
devInfo->hostApi, device);
|
|
return 0;
|
|
}
|
|
|
|
qDebug("PortAudioSystem: using %s %s", apiInfo->name, devInfo->name);
|
|
|
|
PaStreamParameters streamPar;
|
|
streamPar.channelCount = 1;
|
|
|
|
if (!isInput && devInfo->maxOutputChannels > 1) {
|
|
// TODO: add support for more than 2 channels
|
|
streamPar.channelCount = 2;
|
|
}
|
|
|
|
streamPar.device = device;
|
|
streamPar.sampleFormat = paFloat32;
|
|
streamPar.suggestedLatency = (isInput ? devInfo->defaultLowInputLatency : devInfo->defaultLowOutputLatency);
|
|
streamPar.hostApiSpecificStreamInfo = nullptr;
|
|
|
|
const auto ret =
|
|
Pa_OpenStream(stream, isInput ? &streamPar : nullptr, isInput ? nullptr : &streamPar, SAMPLE_RATE, frameSize,
|
|
paClipOff | paDitherOff, &streamCallback, reinterpret_cast< void * >(isInput));
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to open stream - Pa_OpenStream() returned: %s", Pa_GetErrorText(ret));
|
|
*stream = nullptr;
|
|
return 0;
|
|
}
|
|
|
|
return streamPar.channelCount;
|
|
}
|
|
|
|
bool PortAudioSystem::closeStream(PaStream *stream) {
|
|
if (!bOk || !stream) {
|
|
return false;
|
|
}
|
|
|
|
QMutexLocker lock(&qmWait);
|
|
|
|
const auto ret = Pa_CloseStream(stream);
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to close stream - Pa_CloseStream() returned: %s", Pa_GetErrorText(ret));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PortAudioSystem::startStream(PaStream *stream) {
|
|
if (!bOk) {
|
|
return false;
|
|
}
|
|
|
|
if (isStreamRunning(stream)) {
|
|
return true;
|
|
}
|
|
|
|
const auto ret = Pa_StartStream(stream);
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to start stream - Pa_StartStream() returned: %s", Pa_GetErrorText(ret));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool PortAudioSystem::stopStream(PaStream *stream) {
|
|
if (!bOk) {
|
|
return false;
|
|
}
|
|
|
|
if (!isStreamRunning(stream)) {
|
|
return true;
|
|
}
|
|
|
|
const auto ret = Pa_StopStream(stream);
|
|
if (ret != paNoError) {
|
|
qWarning("PortAudioSystem: failed to stop stream - Pa_StopStream() returned: %s", Pa_GetErrorText(ret));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
int PortAudioSystem::streamCallback(const void *inBuffer, void *outBuffer, unsigned long frames,
|
|
const PaStreamCallbackTimeInfo *, PaStreamCallbackFlags, void *isInput) {
|
|
if (isInput) {
|
|
auto const input = dynamic_cast< PortAudioInput * >(Global::get().ai.get());
|
|
if (!input) {
|
|
return paAbort;
|
|
}
|
|
|
|
input->process(static_cast< unsigned int >(frames), inBuffer);
|
|
} else {
|
|
auto const output = dynamic_cast< PortAudioOutput * >(Global::get().ao.get());
|
|
if (!output) {
|
|
return paAbort;
|
|
}
|
|
|
|
output->process(static_cast< unsigned int >(frames), outBuffer);
|
|
}
|
|
|
|
return paContinue;
|
|
}
|
|
|
|
PortAudioInput::PortAudioInput() : stream(nullptr) {
|
|
iMicChannels =
|
|
static_cast< unsigned int >(pas->openStream(&stream, Global::get().s.iPortAudioInput, iFrameSize, true));
|
|
if (!iMicChannels) {
|
|
qWarning("PortAudioInput: failed to open stream");
|
|
return;
|
|
}
|
|
|
|
iMicFreq = SAMPLE_RATE;
|
|
eMicFormat = SampleFloat;
|
|
initializeMixer();
|
|
|
|
if (!pas->startStream(stream)) {
|
|
qWarning("PortAudioInput: failed to start stream");
|
|
}
|
|
}
|
|
|
|
PortAudioInput::~PortAudioInput() {
|
|
// Request interruption
|
|
qmWait.lock();
|
|
|
|
if (!pas->stopStream(stream)) {
|
|
qWarning("PortAudioInput: failed to stop stream");
|
|
}
|
|
|
|
qwcSleep.wakeAll();
|
|
qmWait.unlock();
|
|
|
|
// Wait for thread to exit
|
|
wait();
|
|
|
|
// Cleanup
|
|
if (!pas->closeStream(stream)) {
|
|
qWarning("PortAudioInput: failed to close stream");
|
|
}
|
|
}
|
|
|
|
void PortAudioInput::process(const uint32_t frames, const void *buffer) {
|
|
addMic(buffer, frames);
|
|
}
|
|
|
|
void PortAudioInput::run() {
|
|
if (!pas->isStreamRunning(stream)) {
|
|
return;
|
|
}
|
|
|
|
// Pause thread until interruption is requested by the destructor
|
|
qmWait.lock();
|
|
qwcSleep.wait(&qmWait);
|
|
qmWait.unlock();
|
|
}
|
|
|
|
PortAudioOutput::PortAudioOutput() : stream(nullptr) {
|
|
iChannels =
|
|
static_cast< unsigned int >(pas->openStream(&stream, Global::get().s.iPortAudioOutput, iFrameSize, false));
|
|
if (!iChannels) {
|
|
qWarning("PortAudioOutput: failed to open stream");
|
|
return;
|
|
}
|
|
|
|
iMixerFreq = SAMPLE_RATE;
|
|
eSampleFormat = SampleFloat;
|
|
|
|
const uint32_t channelsMask[]{ SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT };
|
|
|
|
initializeMixer(channelsMask);
|
|
|
|
if (!pas->startStream(stream)) {
|
|
qWarning("PortAudioOutput: failed to start stream");
|
|
}
|
|
}
|
|
|
|
PortAudioOutput::~PortAudioOutput() {
|
|
// Request interruption
|
|
qmWait.lock();
|
|
|
|
if (!pas->stopStream(stream)) {
|
|
qWarning("PortAudioOutput: failed to stop stream");
|
|
}
|
|
|
|
qwcSleep.wakeAll();
|
|
qmWait.unlock();
|
|
|
|
// Wait for thread to exit
|
|
wait();
|
|
|
|
// Cleanup
|
|
if (!pas->closeStream(stream)) {
|
|
qWarning("PortAudioOutput: failed to close stream");
|
|
}
|
|
}
|
|
|
|
void PortAudioOutput::process(const uint32_t frames, void *buffer) {
|
|
if (!mix(buffer, frames)) {
|
|
memset(buffer, 0, sizeof(float) * frames * iChannels);
|
|
}
|
|
}
|
|
|
|
void PortAudioOutput::run() {
|
|
if (!pas->isStreamRunning(stream)) {
|
|
return;
|
|
}
|
|
|
|
// Pause thread until interruption is requested by the destructor
|
|
qmWait.lock();
|
|
qwcSleep.wait(&qmWait);
|
|
qmWait.unlock();
|
|
}
|
|
|
|
#undef RESOLVE
|