mumble-voip_mumble/src/mumble/ALSAAudio.cpp

585 lines
18 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 "ALSAAudio.h"
#include "MainWindow.h"
#include "User.h"
#include "Utils.h"
#include <alsa/asoundlib.h>
#include <poll.h>
#include "Global.h"
#define NBLOCKS 8
class ALSAEnumerator {
public:
QHash< QString, QString > qhInput;
QHash< QString, QString > qhOutput;
static QString getHint(void *hint, const char *id);
ALSAEnumerator();
};
static ALSAEnumerator *cards = nullptr;
class ALSAAudioInputRegistrar : public AudioInputRegistrar {
public:
ALSAAudioInputRegistrar();
virtual AudioInput *create();
virtual const QVariant getDeviceChoice();
virtual const QList< audioDevice > getDeviceChoices();
virtual void setDeviceChoice(const QVariant &, Settings &);
virtual bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const;
virtual bool isMicrophoneAccessDeniedByOS() { return false; };
};
class ALSAAudioOutputRegistrar : public AudioOutputRegistrar {
public:
ALSAAudioOutputRegistrar();
virtual AudioOutput *create();
virtual const QVariant getDeviceChoice();
virtual const QList< audioDevice > getDeviceChoices();
virtual void setDeviceChoice(const QVariant &, Settings &);
};
class ALSAInit : public DeferInit {
protected:
ALSAAudioInputRegistrar *pairALSA;
ALSAAudioOutputRegistrar *paorALSA;
public:
void initialize();
void destroy();
};
static ALSAInit aiInit;
QMutex qmALSA;
void ALSAInit::initialize() {
pairALSA = nullptr;
paorALSA = nullptr;
cards = nullptr;
int card = -1;
snd_card_next(&card);
if (card != -1) {
pairALSA = new ALSAAudioInputRegistrar();
paorALSA = new ALSAAudioOutputRegistrar();
cards = new ALSAEnumerator();
} else {
qWarning("ALSAInit: No cards found, not initializing");
}
}
void ALSAInit::destroy() {
QMutexLocker qml(&qmALSA);
delete pairALSA;
delete paorALSA;
delete cards;
}
ALSAAudioInputRegistrar::ALSAAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("ALSA"), 5) {
}
AudioInput *ALSAAudioInputRegistrar::create() {
return new ALSAAudioInput();
}
const QVariant ALSAAudioInputRegistrar::getDeviceChoice() {
return Global::get().s.qsALSAInput;
}
const QList< audioDevice > ALSAAudioInputRegistrar::getDeviceChoices() {
QList< audioDevice > choices;
QStringList keys = cards->qhInput.keys();
std::sort(keys.begin(), keys.end());
for (const auto &key : keys) {
const auto name = QString::fromLatin1("[%1] %2").arg(key, cards->qhInput.value(key));
choices << audioDevice(name, key);
}
return choices;
}
void ALSAAudioInputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
s.qsALSAInput = choice.toString();
}
bool ALSAAudioInputRegistrar::canEcho(EchoCancelOptionID, const QString &) const {
return false;
}
ALSAAudioOutputRegistrar::ALSAAudioOutputRegistrar() : AudioOutputRegistrar(QLatin1String("ALSA"), 5) {
}
AudioOutput *ALSAAudioOutputRegistrar::create() {
return new ALSAAudioOutput();
}
const QVariant ALSAAudioOutputRegistrar::getDeviceChoice() {
return Global::get().s.qsALSAOutput;
}
const QList< audioDevice > ALSAAudioOutputRegistrar::getDeviceChoices() {
QList< audioDevice > choices;
QStringList keys = cards->qhOutput.keys();
std::sort(keys.begin(), keys.end());
for (const auto &key : keys) {
const auto name = QString::fromLatin1("[%1] %2").arg(key, cards->qhOutput.value(key));
choices << audioDevice(name, key);
}
return choices;
}
void ALSAAudioOutputRegistrar::setDeviceChoice(const QVariant &choice, Settings &s) {
s.qsALSAOutput = choice.toString();
}
ALSAEnumerator::ALSAEnumerator() {
QMutexLocker qml(&qmALSA);
qhInput.insert(QLatin1String("default"), ALSAAudioInput::tr("Default ALSA Card"));
qhOutput.insert(QLatin1String("default"), ALSAAudioOutput::tr("Default ALSA Card"));
#if SND_LIB_VERSION >= 0x01000e
void **hints = nullptr;
void **hint;
snd_config_t *basic = nullptr;
int r;
snd_config_update();
r = snd_config_search(snd_config, "defaults.namehint.extended", &basic);
if ((r == 0) && basic) {
if (snd_config_set_ascii(basic, "on"))
qWarning("ALSAEnumerator: Failed to set namehint");
} else {
qWarning("ALSAEnumerator: Namehint not found");
}
r = snd_device_name_hint(-1, "pcm", &hints);
if (r || !hints) {
qWarning("ALSAEnumerator: snd_device_name_hint: %d", r);
} else {
hint = hints;
while (*hint) {
const QString name = getHint(*hint, "NAME");
const QString ioid = getHint(*hint, "IOID");
QString desc = getHint(*hint, "DESC");
desc.replace(QLatin1Char('\n'), QLatin1Char(' '));
// ALSA, in it's infinite wisdom, claims "dmix" is an input/output device.
// Since there seems to be no way to fetch the ctl interface for a matching device string
// without actually opening it, we'll simply have to start guessing.
bool caninput = (ioid.isNull() || (ioid.compare(QLatin1String("Input"), Qt::CaseInsensitive) == 0));
bool canoutput = (ioid.isNull() || (ioid.compare(QLatin1String("Output"), Qt::CaseInsensitive) == 0));
if (name.startsWith(QLatin1String("dmix:")))
caninput = false;
else if (name.startsWith(QLatin1String("dsnoop:")))
canoutput = false;
if (caninput)
qhInput.insert(name, desc);
if (canoutput)
qhOutput.insert(name, desc);
++hint;
}
snd_device_name_free_hint(hints);
}
snd_config_update_free_global();
snd_config_update();
#else
int card = -1;
snd_card_next(&card);
while (card != -1) {
char *name;
int err;
snd_ctl_t *ctl = nullptr;
if ((err = snd_card_get_longname(card, &name)) != 0) {
Global::get().mw->msgBox(tr("Getting name (longname) of the sound card failed: %1")
.arg(QString::fromUtf8(snd_strerror(err)).toHtmlEscaped()));
return;
}
QByteArray dev = QString::fromLatin1("hw:%1").arg(card).toUtf8();
if (snd_ctl_open(&ctl, dev.data(), SND_CTL_READONLY) >= 0) {
snd_pcm_info_t *info = nullptr;
snd_pcm_info_malloc(&info);
char *cname = nullptr;
if ((err = snd_card_get_name(card, &cname)) != 0) {
Global::get().mw->msgBox(tr("Getting name of the sound card failed: %1")
.arg(QString::fromUtf8(snd_strerror(err)).toHtmlEscaped()));
return;
}
int device = -1;
snd_ctl_pcm_next_device(ctl, &device);
bool play = false;
bool cap = false;
while (device != -1) {
QString devname = QString::fromLatin1("hw:%1,%2").arg(card).arg(device);
snd_pcm_info_set_device(info, device);
snd_pcm_info_set_stream(info, SND_PCM_STREAM_CAPTURE);
if (snd_ctl_pcm_info(ctl, info) == 0) {
QString fname = QString::fromLatin1(snd_pcm_info_get_name(info));
qhInput.insert(devname, fname);
cap = true;
}
snd_pcm_info_set_stream(info, SND_PCM_STREAM_PLAYBACK);
if (snd_ctl_pcm_info(ctl, info) == 0) {
QString fname = QString::fromLatin1(snd_pcm_info_get_name(info));
qhOutput.insert(devname, fname);
play = true;
}
snd_ctl_pcm_next_device(ctl, &device);
}
if (play) {
qhOutput.insert(QString::fromLatin1("dmix:CARD=%1").arg(card), QLatin1String(cname));
}
if (cap) {
qhInput.insert(QString::fromLatin1("dsnoop:CARD=%1").arg(card), QLatin1String(cname));
}
snd_pcm_info_free(info);
snd_ctl_close(ctl);
free(cname);
}
snd_card_next(&card);
free(name);
}
#endif
}
QString ALSAEnumerator::getHint(void *hint, const char *id) {
QString s;
#if SND_LIB_VERSION >= 0x01000e
char *value = snd_device_name_get_hint(hint, id);
if (value) {
s = QLatin1String(value);
free(value);
}
#endif
return s;
}
ALSAAudioInput::ALSAAudioInput() {
bRunning = true;
}
ALSAAudioInput::~ALSAAudioInput() {
// Signal input thread to end
bRunning = false;
wait();
}
#define ALSA_ERRBAIL(x) \
if (!bOk) { \
} else if ((err = static_cast< int >(x)) < 0) { \
bOk = false; \
qWarning("ALSAAudio: %s: %s", #x, snd_strerror(err)); \
}
#define ALSA_ERRCHECK(x) \
if (!bOk) { \
} else if ((err = static_cast< int >(x)) < 0) { \
qWarning("ALSAAudio: Non-critical: %s: %s", #x, snd_strerror(err)); \
}
void ALSAAudioInput::run() {
QMutexLocker qml(&qmALSA);
snd_pcm_sframes_t readblapp;
QByteArray device_name = Global::get().s.qsALSAInput.toLatin1();
snd_pcm_hw_params_t *hw_params = nullptr;
snd_pcm_t *capture_handle = nullptr;
unsigned int rrate = SAMPLE_RATE;
bool bOk = true;
int err = 0;
unsigned int iChannels = 1;
qWarning("ALSAAudioInput: Initing audiocapture %s.", device_name.data());
snd_pcm_hw_params_alloca(&hw_params);
ALSA_ERRBAIL(snd_pcm_open(&capture_handle, device_name.data(), SND_PCM_STREAM_CAPTURE, 0));
ALSA_ERRCHECK(snd_pcm_hw_params_any(capture_handle, hw_params));
ALSA_ERRBAIL(snd_pcm_hw_params_set_access(capture_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED));
ALSA_ERRBAIL(snd_pcm_hw_params_set_format(capture_handle, hw_params, SND_PCM_FORMAT_S16));
ALSA_ERRBAIL(snd_pcm_hw_params_set_rate_near(capture_handle, hw_params, &rrate, nullptr));
ALSA_ERRBAIL(snd_pcm_hw_params_set_channels_near(capture_handle, hw_params, &iChannels));
snd_pcm_uframes_t wantPeriod = (rrate * iFrameSize) / SAMPLE_RATE;
snd_pcm_uframes_t wantBuff = wantPeriod * 8;
ALSA_ERRBAIL(snd_pcm_hw_params_set_period_size_near(capture_handle, hw_params, &wantPeriod, nullptr));
ALSA_ERRBAIL(snd_pcm_hw_params_set_buffer_size_near(capture_handle, hw_params, &wantBuff));
ALSA_ERRBAIL(snd_pcm_hw_params(capture_handle, hw_params));
qWarning("ALSAAudioInput: Actual buffer %d hz, %d channel %ld samples [%ld per period]", rrate, iChannels, wantBuff,
wantPeriod);
ALSA_ERRBAIL(snd_pcm_hw_params_current(capture_handle, hw_params));
ALSA_ERRBAIL(snd_pcm_hw_params_get_channels(hw_params, &iMicChannels));
ALSA_ERRBAIL(snd_pcm_hw_params_get_rate(hw_params, &iMicFreq, nullptr));
#ifdef ALSA_VERBOSE
snd_output_t *log;
snd_output_stdio_attach(&log, stderr, 0);
if (capture_handle)
snd_pcm_dump(capture_handle, log);
#endif
ALSA_ERRBAIL(snd_pcm_prepare(capture_handle));
ALSA_ERRBAIL(snd_pcm_start(capture_handle));
if (!bOk) {
if (capture_handle) {
snd_pcm_drain(capture_handle);
snd_pcm_close(capture_handle);
capture_handle = nullptr;
}
Global::get().mw->msgBox(
tr("Opening chosen ALSA Input failed: %1").arg(QString::fromLatin1(snd_strerror(err)).toHtmlEscaped()));
return;
}
eMicFormat = SampleShort;
initializeMixer();
static std::vector< char > inbuff;
inbuff.resize(wantPeriod * iChannels * sizeof(short));
qml.unlock();
while (bRunning) {
#ifdef ALSA_VERBOSE
snd_pcm_status_malloc(&status);
snd_pcm_status(capture_handle, status);
snd_pcm_status_dump(status, log);
snd_pcm_status_free(status);
#endif
readblapp = snd_pcm_readi(capture_handle, inbuff.data(), static_cast< snd_pcm_uframes_t >(wantPeriod));
if (readblapp == -ESTRPIPE) {
qWarning("ALSAAudioInput: PCM suspended, trying to resume");
while (bRunning && snd_pcm_resume(capture_handle) == -EAGAIN)
msleep(1000);
if ((err = snd_pcm_prepare(capture_handle)) < 0)
qWarning("ALSAAudioInput: %s: %s", snd_strerror(static_cast< int >(readblapp)), snd_strerror(err));
} else if (readblapp == -EPIPE) {
err = snd_pcm_prepare(capture_handle);
qWarning("ALSAAudioInput: %s: %s", snd_strerror(static_cast< int >(readblapp)), snd_strerror(err));
} else if (readblapp < 0) {
err = snd_pcm_prepare(capture_handle);
qWarning("ALSAAudioInput: %s: %s", snd_strerror(static_cast< int >(readblapp)), snd_strerror(err));
} else if (wantPeriod == static_cast< unsigned int >(readblapp)) {
addMic(inbuff.data(), static_cast< unsigned int >(readblapp));
}
}
snd_pcm_drop(capture_handle);
snd_pcm_close(capture_handle);
qWarning("ALSAAudioInput: Releasing ALSA Mic.");
}
ALSAAudioOutput::ALSAAudioOutput() {
qWarning("ALSAAudioOutput: Initialized");
bRunning = true;
}
ALSAAudioOutput::~ALSAAudioOutput() {
bRunning = false;
// Call destructor of all children
wipe();
// Wait for terminate
wait();
qWarning("ALSAAudioOutput: Destroyed");
}
void ALSAAudioOutput::run() {
QMutexLocker qml(&qmALSA);
snd_pcm_t *pcm_handle = nullptr;
struct pollfd fds[16];
int count;
bool stillRun = true;
int err = 0;
bool bOk = true;
snd_pcm_hw_params_t *hw_params = nullptr;
snd_pcm_sw_params_t *sw_params = nullptr;
QByteArray device_name = Global::get().s.qsALSAOutput.toLatin1();
snd_pcm_hw_params_alloca(&hw_params);
snd_pcm_sw_params_alloca(&sw_params);
ALSA_ERRBAIL(snd_pcm_open(&pcm_handle, device_name.data(), SND_PCM_STREAM_PLAYBACK, 0));
ALSA_ERRCHECK(snd_pcm_hw_params_any(pcm_handle, hw_params));
iChannels = 1;
ALSA_ERRBAIL(snd_pcm_hw_params_get_channels_max(hw_params, &iChannels));
if (iChannels > 9) {
qWarning("ALSAAudioOutput: ALSA reports %d output channels. Clamping to 2.", iChannels);
iChannels = 2;
}
ALSA_ERRBAIL(snd_pcm_hw_params_set_access(pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED));
ALSA_ERRBAIL(snd_pcm_hw_params_set_format(pcm_handle, hw_params, SND_PCM_FORMAT_S16));
ALSA_ERRBAIL(snd_pcm_hw_params_set_channels_near(pcm_handle, hw_params, &iChannels));
unsigned int rrate = SAMPLE_RATE;
ALSA_ERRBAIL(snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &rrate, nullptr));
unsigned int iOutputSize = (iFrameSize * rrate) / SAMPLE_RATE;
snd_pcm_uframes_t period_size = iOutputSize;
snd_pcm_uframes_t buffer_size = iOutputSize * static_cast< unsigned int >(Global::get().s.iOutputDelay + 1);
int dir = 1;
ALSA_ERRBAIL(snd_pcm_hw_params_set_period_size_near(pcm_handle, hw_params, &period_size, &dir));
ALSA_ERRBAIL(snd_pcm_hw_params_set_buffer_size_near(pcm_handle, hw_params, &buffer_size));
ALSA_ERRBAIL(snd_pcm_hw_params(pcm_handle, hw_params));
ALSA_ERRBAIL(snd_pcm_hw_params_current(pcm_handle, hw_params));
ALSA_ERRBAIL(snd_pcm_hw_params_get_period_size(hw_params, &period_size, &dir));
ALSA_ERRBAIL(snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_size));
qWarning("ALSAAudioOutput: Actual buffer %d hz, %d channel %ld samples [%ld per period]", rrate, iChannels,
buffer_size, period_size);
ALSA_ERRBAIL(snd_pcm_sw_params_current(pcm_handle, sw_params));
ALSA_ERRBAIL(snd_pcm_sw_params_set_avail_min(pcm_handle, sw_params, period_size));
ALSA_ERRBAIL(snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params, buffer_size - period_size));
ALSA_ERRBAIL(snd_pcm_sw_params_set_stop_threshold(pcm_handle, sw_params, buffer_size));
ALSA_ERRBAIL(snd_pcm_sw_params(pcm_handle, sw_params));
#ifdef ALSA_VERBOSE
snd_output_t *log;
snd_output_stdio_attach(&log, stderr, 0);
if (pcm_handle)
snd_pcm_dump(pcm_handle, log);
#endif
ALSA_ERRBAIL(snd_pcm_prepare(pcm_handle));
const unsigned int buffsize = static_cast< unsigned int >(period_size * iChannels);
static std::vector< float > zerobuff;
zerobuff.resize(buffsize);
static std::vector< float > outbuff;
outbuff.resize(buffsize);
for (unsigned int i = 0; i < buffsize; i++)
zerobuff[i] = 0;
// Fill buffer
if (bOk && pcm_handle)
for (unsigned int i = 0; i < buffer_size / period_size; i++)
snd_pcm_writei(pcm_handle, zerobuff.data(), period_size);
if (!bOk) {
Global::get().mw->msgBox(
tr("Opening chosen ALSA Output failed: %1").arg(QString::fromLatin1(snd_strerror(err)).toHtmlEscaped()));
if (pcm_handle) {
snd_pcm_close(pcm_handle);
pcm_handle = nullptr;
}
return;
}
const unsigned int chanmasks[32] = { SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT, SPEAKER_BACK_LEFT,
SPEAKER_BACK_RIGHT, SPEAKER_FRONT_CENTER, SPEAKER_LOW_FREQUENCY,
SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT, SPEAKER_BACK_CENTER };
ALSA_ERRBAIL(snd_pcm_hw_params_current(pcm_handle, hw_params));
ALSA_ERRBAIL(snd_pcm_hw_params_get_channels(hw_params, &iChannels));
ALSA_ERRBAIL(snd_pcm_hw_params_get_rate(hw_params, &rrate, nullptr));
iMixerFreq = rrate;
eSampleFormat = SampleShort;
qWarning("ALSAAudioOutput: Initializing %d channel, %d hz mixer", iChannels, iMixerFreq);
initializeMixer(chanmasks);
count = snd_pcm_poll_descriptors_count(pcm_handle);
snd_pcm_poll_descriptors(pcm_handle, fds, static_cast< unsigned int >(count));
qml.unlock();
while (bRunning && bOk) {
poll(fds, static_cast< nfds_t >(count), 20);
unsigned short revents;
snd_pcm_poll_descriptors_revents(pcm_handle, fds, static_cast< unsigned int >(count), &revents);
if (revents & POLLERR) {
snd_pcm_prepare(pcm_handle);
} else if (revents & POLLOUT) {
snd_pcm_sframes_t avail{};
ALSA_ERRCHECK(avail = snd_pcm_avail_update(pcm_handle));
while (avail >= static_cast< int >(period_size)) {
stillRun = mix(outbuff.data(), static_cast< unsigned int >(period_size));
if (stillRun) {
snd_pcm_sframes_t w = 0;
ALSA_ERRCHECK(w = snd_pcm_writei(pcm_handle, outbuff.data(), period_size));
if (w < 0) {
avail = w;
break;
}
} else
break;
ALSA_ERRCHECK(avail = snd_pcm_avail_update(pcm_handle));
}
if (avail == -EPIPE) {
snd_pcm_drain(pcm_handle);
ALSA_ERRCHECK(snd_pcm_prepare(pcm_handle));
for (unsigned int i = 0; i < buffer_size / period_size; ++i)
ALSA_ERRCHECK(snd_pcm_writei(pcm_handle, zerobuff.data(), period_size));
}
if (!stillRun) {
snd_pcm_drain(pcm_handle);
while (bRunning && !mix(outbuff.data(), static_cast< unsigned int >(period_size))) {
this->msleep(10);
}
if (!bRunning)
break;
snd_pcm_prepare(pcm_handle);
// Fill one frame
for (unsigned int i = 0; i < (buffer_size / period_size) - 1; i++)
snd_pcm_writei(pcm_handle, zerobuff.data(), period_size);
snd_pcm_writei(pcm_handle, outbuff.data(), period_size);
}
}
}
snd_pcm_close(pcm_handle);
}
#undef NBLOCKS
#undef ALSA_ERRBAIL
#undef ALSA_ERRCHECK