diff options
Diffstat (limited to 'pica-client/capturegain.cpp')
| -rw-r--r-- | pica-client/capturegain.cpp | 533 |
1 files changed, 0 insertions, 533 deletions
diff --git a/pica-client/capturegain.cpp b/pica-client/capturegain.cpp deleted file mode 100644 index 8f89718..0000000 --- a/pica-client/capturegain.cpp +++ /dev/null @@ -1,533 +0,0 @@ -/* - (c) Copyright 2012 - 2026 Anton Sviridenko - https://picapica.im - - This program is free 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, version 3. - - This program 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 this program. If not, see <https://www.gnu.org/licenses/>. -*/ -#include "capturegain.h" - -#include <QDebug> -#include <QtGlobal> -#include <QStringList> -#include <cmath> - -#ifdef Q_OS_LINUX -#include <alsa/asoundlib.h> -#endif - -#ifdef HAVE_LIBPULSE -#include <pulse/pulseaudio.h> -#endif - -// Per platform factories, defined next to the implementations themselves - -// same arrangement as NativeAudioBackend::create(). -#ifdef Q_OS_WIN -CaptureGainControl *pica_create_wasapi_capture_gain(const QString &device); -#endif - -#if defined(Q_OS_MACOS) || defined(Q_OS_MAC) -CaptureGainControl *pica_create_coreaudio_capture_gain(const QString &device); -#endif - -CaptureGainControl::~CaptureGainControl() -{ -} - -static double clampGain(double v) -{ - if (v < 0.0) - return 0.0; - if (v > 1.0) - return 1.0; - return v; -} - -// PulseAudio is the one backend whose control goes past its nominal maximum - -// PA_VOLUME_NORM is 100% and the server will happily sit above it. Clamping a -// reading to 1.0 there would mean handing a user who had their source at 130% -// back at 100% when the call ended, so that path keeps whatever it read and -// only guards the bottom of the range and a runaway top. -static double clampGainAmplified(double v) -{ - if (v < 0.0) - return 0.0; - if (v > 2.5) - return 2.5; - return v; -} - -#ifdef Q_OS_LINUX - -// The mixer lives on a card, but what we are handed is a PCM name. The two -// namespaces overlap only by convention, so this covers the forms that -// actually turn up in the device list and falls back to the default card for -// anything else - which is right for plain "default" and no worse than -// nothing for the exotic cases. -static QString alsaCardFromPcm(const QString &pcm) -{ - int card = pcm.indexOf(QLatin1String("CARD=")); - if (card >= 0) - { - QString name = pcm.mid(card + 5); - int comma = name.indexOf(QLatin1Char(',')); - if (comma >= 0) - name = name.left(comma); - if (!name.isEmpty()) - return QStringLiteral("hw:") + name; - } - - // "hw:1,0", "plughw:CARD=PCH,DEV=0", "dsnoop:1" - everything before the - // first comma after the colon names the card. - int colon = pcm.indexOf(QLatin1Char(':')); - if (colon >= 0) - { - QString rest = pcm.mid(colon + 1); - int comma = rest.indexOf(QLatin1Char(',')); - if (comma >= 0) - rest = rest.left(comma); - if (!rest.isEmpty()) - return QStringLiteral("hw:") + rest; - } - - return QStringLiteral("default"); -} - -class AlsaCaptureGain : public CaptureGainControl -{ -public: - AlsaCaptureGain() : m_mixer(nullptr), m_elem(nullptr), m_min(0), m_max(0) {} - - ~AlsaCaptureGain() override - { - if (m_mixer) - snd_mixer_close(m_mixer); - } - - bool open(const QString &pcmName) - { - // The card the PCM name points at, and then the real cards as - // fallbacks. "default" is not a card at all on a machine running a - // sound server: attaching to it lands on the server's own software - // volume, which is applied to samples the converter has already - // digitised. That control cannot undo clipping - scaling a clipped - // buffer down just gives a quieter clipped buffer - so calibrating - // against it would turn the microphone down forever without the - // clipping ever going away. Only a gain ahead of the ADC is any use - // here, and tryCard() insists on one. - QStringList candidates; - candidates << alsaCardFromPcm(pcmName); - - if (candidates.first() == QLatin1String("default")) - { - for (int card = 0; card < 8; card++) - candidates << (QStringLiteral("hw:") + QString::number(card)); - } - - for (int i = 0; i < candidates.size(); i++) - { - if (tryCard(candidates[i])) - return true; - } - - return false; - } - - bool gain(double *out) override - { - if (!m_elem || !out) - return false; - - // The kernel may have moved the control under us - another mixer app, - // or the user's own volume keys. - snd_mixer_handle_events(m_mixer); - - long v = 0; - if (snd_mixer_selem_get_capture_volume(m_elem, SND_MIXER_SCHN_FRONT_LEFT, &v) < 0 && - snd_mixer_selem_get_capture_volume(m_elem, SND_MIXER_SCHN_MONO, &v) < 0) - return false; - - *out = clampGain(double(v - m_min) / double(m_max - m_min)); - return true; - } - - bool gainDb(double *out) override - { - if (!m_elem || !out) - return false; - - snd_mixer_handle_events(m_mixer); - - // ALSA reports hundredths of a decibel. Not every element has a dB - // mapping - the software controls a sound server exposes often do not. - long v = 0; - if (snd_mixer_selem_get_capture_dB(m_elem, SND_MIXER_SCHN_FRONT_LEFT, &v) < 0 && - snd_mixer_selem_get_capture_dB(m_elem, SND_MIXER_SCHN_MONO, &v) < 0) - return false; - - *out = double(v) / 100.0; - return true; - } - - bool setGain(double value) override - { - if (!m_elem) - return false; - - long v = m_min + (long)llround(clampGain(value) * double(m_max - m_min)); - return snd_mixer_selem_set_capture_volume_all(m_elem, v) >= 0; - } - - QString description() const override { return m_name; } - -private: - // A candidate has to have a decibel range as well as a volume range. That - // is what separates a converter gain from a sound server's software fader: - // the hardware control knows what its steps are worth in dB, the software - // one only has a slider position. It is also what lets the caller bound - // the total cut in dB, which is the only unit that means the same thing on - // every backend. - bool tryCard(const QString &card) - { - if (m_mixer) - { - snd_mixer_close(m_mixer); - m_mixer = nullptr; - m_elem = nullptr; - } - - if (snd_mixer_open(&m_mixer, 0) < 0) - { - m_mixer = nullptr; - return false; - } - - const QByteArray cardUtf8 = card.toUtf8(); - if (snd_mixer_attach(m_mixer, cardUtf8.constData()) < 0 || - snd_mixer_selem_register(m_mixer, nullptr, nullptr) < 0 || - snd_mixer_load(m_mixer) < 0) - return false; - - // "Capture" is what the recording gain is called on essentially every - // card that has one; "Mic" turns up on the ones that do not separate - // the input selector from the gain. - m_elem = findNamed("Capture"); - if (!m_elem) - m_elem = findNamed("Mic"); - if (!m_elem) - m_elem = findNamed("Digital"); - - if (!m_elem) - return false; - - if (snd_mixer_selem_get_capture_volume_range(m_elem, &m_min, &m_max) < 0 || m_max <= m_min) - return false; - - long dbMin = 0, dbMax = 0; - if (snd_mixer_selem_get_capture_dB_range(m_elem, &dbMin, &dbMax) < 0 || dbMax <= dbMin) - { - m_elem = nullptr; - return false; - } - - m_name = QString::fromUtf8(snd_mixer_selem_get_name(m_elem)) + - QStringLiteral(" on ") + card; - return true; - } - - snd_mixer_elem_t *findNamed(const char *want) - { - for (snd_mixer_elem_t *e = snd_mixer_first_elem(m_mixer); e; e = snd_mixer_elem_next(e)) - { - if (!snd_mixer_selem_is_active(e) || !snd_mixer_selem_has_capture_volume(e)) - continue; - if (qstrcmp(snd_mixer_selem_get_name(e), want) == 0) - return e; - } - return nullptr; - } - - snd_mixer_t *m_mixer; - snd_mixer_elem_t *m_elem; - long m_min; - long m_max; - QString m_name; -}; - -#endif // Q_OS_LINUX - -#ifdef HAVE_LIBPULSE - -// PulseAudio's introspection and its setters are both asynchronous, so this -// keeps a connected context with a private mainloop and pumps it until each -// operation has called back. Same shape as enumeratePulse() in -// audiodevice.cpp, but long lived, since a call makes several of these. -class PulseCaptureGain : public CaptureGainControl -{ -public: - PulseCaptureGain() - : m_mainloop(nullptr), m_ctx(nullptr), m_channels(1), - m_pending(false), m_ok(false), m_value(0.0), m_valueDb(0.0) {} - - ~PulseCaptureGain() override - { - if (m_ctx) - { - pa_context_disconnect(m_ctx); - pa_context_unref(m_ctx); - } - if (m_mainloop) - pa_mainloop_free(m_mainloop); - } - - bool open(const QString &device) - { - m_mainloop = pa_mainloop_new(); - if (!m_mainloop) - return false; - - m_ctx = pa_context_new(pa_mainloop_get_api(m_mainloop), "pica-client"); - if (!m_ctx) - return false; - - if (pa_context_connect(m_ctx, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) - return false; - - // Five seconds at 50ms a turn, so a server that never answers cannot - // wedge the call setup. - for (int i = 0; i < 100; i++) - { - pa_context_state_t st = pa_context_get_state(m_ctx); - if (st == PA_CONTEXT_READY) - break; - if (st == PA_CONTEXT_FAILED || st == PA_CONTEXT_TERMINATED) - return false; - if (!pump()) - return false; - } - - if (pa_context_get_state(m_ctx) != PA_CONTEXT_READY) - return false; - - m_source = device; - if (m_source.isEmpty() || m_source == QLatin1String("default")) - { - if (!resolveDefaultSource()) - return false; - } - - // Also establishes the channel count, which setGain() needs. - double ignored; - return gain(&ignored); - } - - bool gain(double *out) override - { - if (!m_ctx || m_source.isEmpty()) - return false; - - const QByteArray name = m_source.toUtf8(); - m_pending = true; - m_ok = false; - - pa_operation *op = pa_context_get_source_info_by_name(m_ctx, name.constData(), - sourceInfoCb, this); - if (!await(op)) - return false; - - if (m_ok && out) - *out = m_value; - return m_ok; - } - - // pa_sw_volume_to_dB() returns -inf for a muted control, which is true but - // useless as a number to do arithmetic on. - bool gainDb(double *out) override - { - if (!out) - return false; - - double ignored; - if (!gain(&ignored)) - return false; - - if (m_valueDb < -200.0 || m_valueDb > 200.0) - return false; - - *out = m_valueDb; - return true; - } - - bool setGain(double value) override - { - if (!m_ctx || m_source.isEmpty()) - return false; - - pa_cvolume cv; - pa_cvolume_set(&cv, m_channels ? m_channels : 1, - (pa_volume_t)llround(clampGainAmplified(value) * double(PA_VOLUME_NORM))); - - const QByteArray name = m_source.toUtf8(); - m_pending = true; - m_ok = false; - - pa_operation *op = pa_context_set_source_volume_by_name(m_ctx, name.constData(), &cv, - successCb, this); - if (!await(op)) - return false; - - return m_ok; - } - - QString description() const override - { - return m_source + QStringLiteral(" (PulseAudio)"); - } - -private: - bool pump() - { - if (pa_mainloop_prepare(m_mainloop, 50 * 1000) < 0) - return false; - if (pa_mainloop_poll(m_mainloop) < 0) - return false; - if (pa_mainloop_dispatch(m_mainloop) < 0) - return false; - return true; - } - - // Runs the loop until the callback for op has fired, then reaps it. - bool await(pa_operation *op) - { - if (!op) - { - m_pending = false; - return false; - } - - for (int i = 0; i < 100 && m_pending; i++) - { - if (!pump()) - break; - } - - bool finished = !m_pending; - m_pending = false; - pa_operation_unref(op); - return finished; - } - - bool resolveDefaultSource() - { - m_pending = true; - m_ok = false; - - pa_operation *op = pa_context_get_server_info(m_ctx, serverInfoCb, this); - if (!await(op)) - return false; - - return m_ok && !m_source.isEmpty(); - } - - static void serverInfoCb(pa_context *, const pa_server_info *info, void *userdata) - { - PulseCaptureGain *self = (PulseCaptureGain *)userdata; - - if (info && info->default_source_name) - { - self->m_source = QString::fromUtf8(info->default_source_name); - self->m_ok = true; - } - self->m_pending = false; - } - - static void sourceInfoCb(pa_context *, const pa_source_info *info, int eol, void *userdata) - { - PulseCaptureGain *self = (PulseCaptureGain *)userdata; - - if (eol) - { - self->m_pending = false; - return; - } - - if (!info) - return; - - self->m_channels = info->volume.channels; - // The loudest channel, so a step down is a step down for all of them - // rather than only for whichever happens to be quietest. - self->m_value = clampGainAmplified(double(pa_cvolume_max(&info->volume)) / double(PA_VOLUME_NORM)); - self->m_valueDb = pa_sw_volume_to_dB(pa_cvolume_max(&info->volume)); - self->m_ok = true; - } - - static void successCb(pa_context *, int success, void *userdata) - { - PulseCaptureGain *self = (PulseCaptureGain *)userdata; - self->m_ok = (success != 0); - self->m_pending = false; - } - - pa_mainloop *m_mainloop; - pa_context *m_ctx; - QString m_source; - uint8_t m_channels; - - bool m_pending; - bool m_ok; - double m_value; - double m_valueDb; -}; - -#endif // HAVE_LIBPULSE - -CaptureGainControl *CaptureGainControl::create(const QString &driver, const QString &device) -{ -#ifdef Q_OS_WIN - if (driver == QLatin1String("platform-wasapi")) - return pica_create_wasapi_capture_gain(device); -#endif - -#if defined(Q_OS_MACOS) || defined(Q_OS_MAC) - if (driver == QLatin1String("platform-coreaudio")) - return pica_create_coreaudio_capture_gain(device); -#endif - -#ifdef HAVE_LIBPULSE - if (driver == QLatin1String("pulse")) - { - PulseCaptureGain *g = new PulseCaptureGain(); - if (g->open(device)) - return g; - delete g; - qWarning() << "No PulseAudio volume control for capture source" << device; - return nullptr; - } -#endif - -#ifdef Q_OS_LINUX - if (driver == QLatin1String("alsa")) - { - AlsaCaptureGain *g = new AlsaCaptureGain(); - if (g->open(device)) - return g; - delete g; - qWarning() << "No ALSA capture volume control for device" << device; - return nullptr; - } -#endif - - Q_UNUSED(driver) - Q_UNUSED(device) - return nullptr; -} |
