summaryrefslogtreecommitdiffstats
path: root/pica-client/audiodevice.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client/audiodevice.cpp')
-rw-r--r--pica-client/audiodevice.cpp783
1 files changed, 745 insertions, 38 deletions
diff --git a/pica-client/audiodevice.cpp b/pica-client/audiodevice.cpp
index 28f5d0a..f0238e4 100644
--- a/pica-client/audiodevice.cpp
+++ b/pica-client/audiodevice.cpp
@@ -15,15 +15,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "audiodevice.h"
+#include "nativeaudio.h"
#include <QDebug>
#include <QMutexLocker>
+#include <QVector>
#include <cstring>
#ifdef Q_OS_LINUX
#include <alsa/asoundlib.h>
#endif
+#ifdef HAVE_LIBPULSE
+#include <pulse/pulseaudio.h>
+#endif
+
// FFmpeg headers
extern "C" {
#include <libavformat/avformat.h>
@@ -48,6 +54,15 @@ extern "C" {
# define AD_HAVE_CH_LAYOUT 0
#endif
+// FFmpeg 7.1 (libavcodec 61.13.100) added avcodec_get_supported_config() and
+// deprecated the AVCodec fields listing what a codec supports; FFmpeg 9.0
+// removed those fields. See chooseEncoderSampleFmt().
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 13, 100)
+# define AD_HAVE_SUPPORTED_CONFIG 1
+#else
+# define AD_HAVE_SUPPORTED_CONFIG 0
+#endif
+
// Every call this codebase currently negotiates is mono - matches
// TonePlayer's design and keeps the resampling/framing code below simple.
static const int kChannels = 1;
@@ -56,6 +71,14 @@ static const int kChannels = 1;
// a reasonable fixed default for a single speech channel.
static const int64_t kOpusBitrate = 32000;
+// The FFmpeg driver name used on Linux. Kept in a process wide variable
+// rather than read from the settings database on demand, because the audio
+// loops run on their own threads and QSqlDatabase connections belong to the
+// thread that opened them. SetLinuxDriverName() is called from the GUI
+// thread; see main.cpp and SettingsDialog::storeSettings().
+static QMutex s_driverMutex;
+static QString s_linuxDriver = QStringLiteral("alsa");
+
static QString ff_errstr(int err)
{
char buf[256] = {0};
@@ -109,6 +132,47 @@ static SwrContext *makeMonoResampler(AVSampleFormat inFmt, int inRate, AVSampleF
return swr;
}
+// Which sample format to run the Opus encoder in. Interleaved 16 bit is
+// preferred over whatever the encoder lists first, because that is the format
+// the echo canceller works in - taking it means the microphone samples can go
+// through the canceller in place on their way to the encoder rather than
+// being converted twice around it. libopus offers it; the native "opus"
+// encoder does not, and on that one the AEC has to convert.
+static AVSampleFormat chooseEncoderSampleFmt(const AVCodec *enc)
+{
+ if (!enc)
+ return AV_SAMPLE_FMT_S16;
+
+ const enum AVSampleFormat *fmts = nullptr;
+
+#if AD_HAVE_SUPPORTED_CONFIG
+ // FFmpeg 7.1 (libavcodec 61.13.100) replaced AVCodec's arrays of
+ // supported formats with this query and deprecated the fields; 9.0
+ // removed the fields outright, which is what MXE builds against.
+ const void *configs = nullptr;
+ if (avcodec_get_supported_config(nullptr, enc, AV_CODEC_CONFIG_SAMPLE_FORMAT,
+ 0, &configs, nullptr) < 0)
+ return AV_SAMPLE_FMT_S16;
+
+ fmts = (const enum AVSampleFormat *)configs;
+#else
+ fmts = enc->sample_fmts;
+#endif
+
+ // No list at all means the encoder takes whatever it is given - so it
+ // takes the one we would rather have.
+ if (!fmts || fmts[0] == AV_SAMPLE_FMT_NONE)
+ return AV_SAMPLE_FMT_S16;
+
+ for (int i = 0; fmts[i] != AV_SAMPLE_FMT_NONE; i++)
+ {
+ if (fmts[i] == AV_SAMPLE_FMT_S16)
+ return AV_SAMPLE_FMT_S16;
+ }
+
+ return fmts[0];
+}
+
AudioDevice::AudioDevice(QObject *parent)
: QObject(parent), m_sampleRate(48000), m_abort(0),
m_lastPlayedSeq(0), m_havePlayedSeq(false)
@@ -134,6 +198,11 @@ void AudioDevice::configurePlayback(QString deviceName, QString codec, int sampl
m_sampleRate = sampleRate;
}
+void AudioDevice::setEchoCanceller(EchoCancellerPtr ec)
+{
+ m_echoCanceller = ec;
+}
+
// Difference between two sequence numbers, correct across the point where
// they wrap around: positive if a comes after b.
static inline int seq_diff(quint16 a, quint16 b)
@@ -192,10 +261,62 @@ void AudioDevice::Close()
m_queueCond.wakeAll();
}
+void AudioDevice::waitForPreroll()
+{
+ // Preroll: wait for a small cushion of buffered packets before writing
+ // the first one, so ordinary arrival jitter doesn't immediately drain
+ // the queue and starve the device buffer (heard as crackling/xruns).
+ QMutexLocker locker(&m_queueMutex);
+
+ while (m_playQueue.size() < kPrerollDepth && !m_abort.loadRelaxed())
+ m_queueCond.wait(&m_queueMutex, 100);
+}
+
+bool AudioDevice::nextPacket(QByteArray *out)
+{
+ QMutexLocker locker(&m_queueMutex);
+
+ while (m_playQueue.isEmpty() && !m_abort.loadRelaxed())
+ m_queueCond.wait(&m_queueMutex, 100);
+
+ if (m_playQueue.isEmpty())
+ return !m_abort.loadRelaxed();
+
+ PlaybackPacket p = m_playQueue.takeFirst();
+
+ *out = p.data;
+ m_lastPlayedSeq = p.seq;
+ m_havePlayedSeq = true;
+
+ return true;
+}
+
void AudioDevice::Capture()
{
m_abort.storeRelaxed(0);
+ const QString driver = AudioDevice::PlatformDriverName(CAPTURE);
+
+ if (AudioDevice::IsPlatformDriver(driver))
+ captureNative(AudioDevice::PlatformApiName(driver));
+ else
+ captureFFmpeg(driver);
+}
+
+void AudioDevice::Play()
+{
+ m_abort.storeRelaxed(0);
+
+ const QString driver = AudioDevice::PlatformDriverName(PLAYBACK);
+
+ if (AudioDevice::IsPlatformDriver(driver))
+ playNative(AudioDevice::PlatformApiName(driver));
+ else
+ playFFmpeg(driver);
+}
+
+void AudioDevice::captureFFmpeg(const QString &driver)
+{
avdevice_register_all();
#if LIBAVFORMAT_VERSION_MAJOR < 58
av_register_all();
@@ -203,11 +324,11 @@ void AudioDevice::Capture()
#endif
const QByteArray deviceUtf8 = m_deviceName.toUtf8();
- const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8();
+ const QByteArray driverUtf8 = driver.toUtf8();
const AVInputFormat *ifmt = av_find_input_format(driverUtf8.constData());
if (!ifmt)
{
- QString msg = QString("Could not find '%1' input driver").arg(AudioDevice::PlatformDriverName());
+ QString msg = QString("Could not find '%1' input driver").arg(driver);
qWarning() << msg;
emit errorOccurred(msg);
return;
@@ -238,6 +359,18 @@ void AudioDevice::Capture()
}
AVStream *in_st = ifmt_ctx->streams[0];
+
+ emit deviceFormatInUse(QString("%1 in %2 Hz %3 ch %4 -> %5 Hz %6 ch")
+ .arg(driver)
+ .arg(in_st->codecpar->sample_rate)
+#if AD_HAVE_CH_LAYOUT
+ .arg(in_st->codecpar->ch_layout.nb_channels)
+#else
+ .arg(in_st->codecpar->channels)
+#endif
+ .arg(QLatin1String(avcodec_get_name(in_st->codecpar->codec_id)))
+ .arg(m_sampleRate).arg(kChannels));
+
const AVCodec *pcm_dec = avcodec_find_decoder(in_st->codecpar->codec_id);
AVCodecContext *dec_ctx = pcm_dec ? avcodec_alloc_context3(pcm_dec) : nullptr;
if (!pcm_dec || !dec_ctx ||
@@ -267,7 +400,7 @@ void AudioDevice::Capture()
}
enc_ctx->sample_rate = m_sampleRate;
- enc_ctx->sample_fmt = enc->sample_fmts ? enc->sample_fmts[0] : AV_SAMPLE_FMT_S16;
+ enc_ctx->sample_fmt = chooseEncoderSampleFmt(enc);
enc_ctx->bit_rate = kOpusBitrate;
enc_ctx->time_base = AVRational{1, m_sampleRate};
applyMonoLayout(enc_ctx);
@@ -304,6 +437,20 @@ void AudioDevice::Capture()
AVAudioFifo *fifo = av_audio_fifo_alloc(enc_ctx->sample_fmt, kChannels, 1);
int frame_size = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 960;
+ // The canceller only gets to work in place when the encoder is taking
+ // interleaved 16 bit, which libopus does. On the native encoder, whose
+ // only format is planar float, cancelling would mean converting to 16 bit
+ // and back around every frame; not worth it for a fallback path.
+ EchoCancellerPtr ec = m_echoCanceller;
+ if (ec && (!ec->isValid() || enc_ctx->sample_fmt != AV_SAMPLE_FMT_S16 ||
+ ec->sampleRate() != m_sampleRate))
+ {
+ if (ec->isValid())
+ qWarning() << "Echo cancellation disabled: encoder format" << av_get_sample_fmt_name(enc_ctx->sample_fmt)
+ << "at" << m_sampleRate << "Hz is not what the canceller was built for";
+ ec.clear();
+ }
+
AVPacket *in_pkt = av_packet_alloc();
AVFrame *dec_frame = av_frame_alloc();
AVPacket *out_pkt = av_packet_alloc();
@@ -371,6 +518,13 @@ void AudioDevice::Capture()
av_frame_get_buffer(enc_frame, 0);
av_audio_fifo_read(fifo, (void **)enc_frame->data, frame_size);
+
+ // Take the echo of what the playback direction has been
+ // feeding the speaker back out of the microphone signal,
+ // before it is encoded and sent.
+ if (ec)
+ ec->processNearEnd((qint16 *)enc_frame->data[0], frame_size);
+
enc_frame->pts = pts;
pts += frame_size;
@@ -402,10 +556,189 @@ void AudioDevice::Capture()
avformat_close_input(&ifmt_ctx);
}
-void AudioDevice::Play()
+void AudioDevice::captureNative(const QString &api)
{
- m_abort.storeRelaxed(0);
+ NativeAudioBackend *backend = NativeAudioBackend::create(api);
+ if (!backend)
+ {
+ QString msg = QString("No '%1' audio backend in this build").arg(api);
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ return;
+ }
+
+ QString err;
+ if (!backend->openCapture(m_deviceName, m_sampleRate, kChannels, NativeAudioVoiceCall, &err))
+ {
+ qWarning() << err;
+ emit errorOccurred(err);
+ delete backend;
+ return;
+ }
+
+ emit deviceFormatInUse(backend->formatDescription());
+ const AVCodec *enc = avcodec_find_encoder_by_name("libopus");
+ if (!enc)
+ enc = avcodec_find_encoder(AV_CODEC_ID_OPUS);
+ AVCodecContext *enc_ctx = enc ? avcodec_alloc_context3(enc) : nullptr;
+ if (!enc || !enc_ctx)
+ {
+ QString msg = "Opus encoder not available";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ if (enc_ctx) avcodec_free_context(&enc_ctx);
+ backend->close();
+ delete backend;
+ return;
+ }
+
+ enc_ctx->sample_rate = m_sampleRate;
+ enc_ctx->sample_fmt = chooseEncoderSampleFmt(enc);
+ enc_ctx->bit_rate = kOpusBitrate;
+ enc_ctx->time_base = AVRational{1, m_sampleRate};
+ applyMonoLayout(enc_ctx);
+ av_opt_set(enc_ctx->priv_data, "application", "voip", 0);
+
+ int ret = avcodec_open2(enc_ctx, enc, nullptr);
+ if (ret < 0)
+ {
+ QString msg = QString("Could not open Opus encoder: %1").arg(ff_errstr(ret));
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&enc_ctx);
+ backend->close();
+ delete backend;
+ return;
+ }
+
+ // The backend delivers interleaved 16 bit at our rate. Anything but an
+ // encoder that takes the same needs a converter in between.
+ SwrContext *swr = nullptr;
+ if (enc_ctx->sample_fmt != AV_SAMPLE_FMT_S16)
+ {
+ swr = makeMonoResampler(AV_SAMPLE_FMT_S16, m_sampleRate, enc_ctx->sample_fmt, m_sampleRate);
+ if (!swr)
+ {
+ QString msg = "Could not set up capture converter";
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ avcodec_free_context(&enc_ctx);
+ backend->close();
+ delete backend;
+ return;
+ }
+ }
+
+ // Nothing for us to do when the platform has already taken the echo out
+ // below us - running a second canceller over an already cancelled signal
+ // only gives it a near silent reference to chase and can chew holes in
+ // the speech.
+ EchoCancellerPtr ec = m_echoCanceller;
+ if (backend->hasPlatformEchoCancellation())
+ {
+ if (ec)
+ qDebug() << api << "provides echo cancellation - not running ours as well";
+ ec.clear();
+ }
+ else if (ec && (!ec->isValid() || ec->sampleRate() != m_sampleRate))
+ {
+ ec.clear();
+ }
+
+ const int frame_size = enc_ctx->frame_size > 0 ? enc_ctx->frame_size : 960;
+
+ QVector<qint16> pending;
+ QVector<qint16> scratch(frame_size * 4);
+ QVector<qint16> block(frame_size);
+
+ AVPacket *out_pkt = av_packet_alloc();
+ int64_t pts = 0;
+ bool loggedEncodeError = false;
+
+ while (!m_abort.loadRelaxed())
+ {
+ int got = backend->read(scratch.data(), scratch.size(), 100);
+ if (got < 0)
+ {
+ qWarning() << "Native capture read failed";
+ break;
+ }
+ if (got == 0)
+ continue;
+
+ int base = pending.size();
+ pending.resize(base + got);
+ memcpy(pending.data() + base, scratch.constData(), got * sizeof(qint16));
+
+ int consumed = 0;
+ while (pending.size() - consumed >= frame_size)
+ {
+ memcpy(block.data(), pending.constData() + consumed, frame_size * sizeof(qint16));
+ consumed += frame_size;
+
+ if (ec)
+ ec->processNearEnd(block.data(), frame_size);
+
+ AVFrame *enc_frame = av_frame_alloc();
+ enc_frame->format = enc_ctx->sample_fmt;
+ enc_frame->sample_rate = enc_ctx->sample_rate;
+ enc_frame->nb_samples = frame_size;
+ applyMonoLayout(enc_frame);
+
+ if (av_frame_get_buffer(enc_frame, 0) < 0)
+ {
+ av_frame_free(&enc_frame);
+ continue;
+ }
+
+ if (swr)
+ {
+ const uint8_t *in[1] = { (const uint8_t *)block.constData() };
+ if (swr_convert(swr, enc_frame->data, frame_size, in, frame_size) <= 0)
+ {
+ av_frame_free(&enc_frame);
+ continue;
+ }
+ }
+ else
+ {
+ memcpy(enc_frame->data[0], block.constData(), frame_size * sizeof(qint16));
+ }
+
+ enc_frame->pts = pts;
+ pts += frame_size;
+
+ if (avcodec_send_frame(enc_ctx, enc_frame) == 0)
+ {
+ while (avcodec_receive_packet(enc_ctx, out_pkt) == 0)
+ {
+ emit packetReady(QByteArray((const char *)out_pkt->data, out_pkt->size));
+ av_packet_unref(out_pkt);
+ }
+ }
+ else if (!loggedEncodeError)
+ {
+ qWarning() << "Opus encode failed";
+ loggedEncodeError = true;
+ }
+
+ av_frame_free(&enc_frame);
+ }
+
+ if (consumed > 0)
+ pending.remove(0, consumed);
+ }
+
+ av_packet_free(&out_pkt);
+ if (swr) swr_free(&swr);
+ avcodec_free_context(&enc_ctx);
+ backend->close();
+ delete backend;
+}
+
+void AudioDevice::playFFmpeg(const QString &driver)
+{
avdevice_register_all();
#if LIBAVFORMAT_VERSION_MAJOR < 58
av_register_all();
@@ -414,12 +747,12 @@ void AudioDevice::Play()
AVFormatContext *oc = nullptr;
const QByteArray deviceUtf8 = m_deviceName.toUtf8();
- const QByteArray driverUtf8 = AudioDevice::PlatformDriverName().toUtf8();
+ const QByteArray driverUtf8 = driver.toUtf8();
int ret = avformat_alloc_output_context2(&oc, nullptr, driverUtf8.constData(), deviceUtf8.constData());
if (ret < 0 || !oc)
{
- QString msg = QString("Could not allocate '%1' output context: %2").arg(AudioDevice::PlatformDriverName(), ff_errstr(ret));
+ QString msg = QString("Could not allocate '%1' output context: %2").arg(driver, ff_errstr(ret));
qWarning() << msg;
emit errorOccurred(msg);
return;
@@ -467,7 +800,7 @@ void AudioDevice::Play()
{
if ((ret = avio_open(&oc->pb, deviceUtf8.constData(), AVIO_FLAG_WRITE)) < 0)
{
- QString msg = QString("Could not open '%1' device '%2': %3").arg(AudioDevice::PlatformDriverName(), m_deviceName, ff_errstr(ret));
+ QString msg = QString("Could not open '%1' device '%2': %3").arg(driver, m_deviceName, ff_errstr(ret));
qWarning() << msg;
emit errorOccurred(msg);
avcodec_free_context(&out_ctx);
@@ -508,6 +841,10 @@ void AudioDevice::Play()
return;
}
+ EchoCancellerPtr ec = m_echoCanceller;
+ if (ec && (!ec->isValid() || ec->sampleRate() != m_sampleRate))
+ ec.clear();
+
SwrContext *swr = nullptr;
AVPacket *out_pkt = av_packet_alloc();
AVFrame *dec_frame = av_frame_alloc();
@@ -516,35 +853,15 @@ void AudioDevice::Play()
bool loggedResamplerError = false;
bool loggedEncodeError = false;
- // Preroll: wait for a small cushion of buffered packets before writing
- // the first one, so ordinary arrival jitter doesn't immediately drain
- // the queue and starve the ALSA buffer (heard as crackling/xruns).
- {
- QMutexLocker locker(&m_queueMutex);
- while (m_playQueue.size() < kPrerollDepth && !m_abort.loadRelaxed())
- m_queueCond.wait(&m_queueMutex, 100);
- }
+ waitForPreroll();
while (true)
{
QByteArray pktData;
- {
- QMutexLocker locker(&m_queueMutex);
- while (m_playQueue.isEmpty() && !m_abort.loadRelaxed())
- m_queueCond.wait(&m_queueMutex, 100);
-
- if (m_abort.loadRelaxed() && m_playQueue.isEmpty())
- break;
-
- if (m_playQueue.isEmpty())
- continue;
-
- PlaybackPacket p = m_playQueue.takeFirst();
-
- pktData = p.data;
- m_lastPlayedSeq = p.seq;
- m_havePlayedSeq = true;
- }
+ if (!nextPacket(&pktData))
+ break;
+ if (pktData.isEmpty())
+ continue;
AVPacket *in_pkt = av_packet_alloc();
if (av_new_packet(in_pkt, pktData.size()) < 0)
@@ -602,6 +919,12 @@ void AudioDevice::Play()
out_frame->pts = pts;
pts += converted_samples;
+ // Hand the canceller the far end signal at the same
+ // moment the sound card gets it, so the delay it has to
+ // model is only the device buffer plus the room.
+ if (ec)
+ ec->pushFarEnd((const qint16 *)out_frame->data[0], converted_samples);
+
if (avcodec_send_frame(out_ctx, out_frame) == 0)
{
while (avcodec_receive_packet(out_ctx, out_pkt) == 0)
@@ -634,12 +957,321 @@ void AudioDevice::Play()
avformat_free_context(oc);
}
-QList<MediaDeviceInfo> AudioDevice::Enumerate(enum MediaDeviceStreamDirection dir)
+void AudioDevice::playNative(const QString &api)
+{
+ NativeAudioBackend *backend = NativeAudioBackend::create(api);
+ if (!backend)
+ {
+ QString msg = QString("No '%1' audio backend in this build").arg(api);
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ return;
+ }
+
+ QString err;
+ if (!backend->openPlayback(m_deviceName, m_sampleRate, kChannels, NativeAudioVoiceCall, &err))
+ {
+ qWarning() << err;
+ emit errorOccurred(err);
+ delete backend;
+ return;
+ }
+
+ emit deviceFormatInUse(backend->formatDescription());
+
+ const AVCodec *in_codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
+ AVCodecContext *dec_ctx = in_codec ? avcodec_alloc_context3(in_codec) : nullptr;
+ if (dec_ctx)
+ {
+ dec_ctx->sample_rate = m_sampleRate;
+ applyMonoLayout(dec_ctx);
+ }
+ if (m_codec != QLatin1String("opus") || !in_codec || !dec_ctx || avcodec_open2(dec_ctx, in_codec, nullptr) < 0)
+ {
+ QString msg = QString("Unsupported call audio codec: %1").arg(m_codec);
+ qWarning() << msg;
+ emit errorOccurred(msg);
+ if (dec_ctx) avcodec_free_context(&dec_ctx);
+ backend->close();
+ delete backend;
+ return;
+ }
+
+ // On a platform that cancels for us, the far end signal it needs is the
+ // one it is rendering itself - there is nothing to hand to ours, and
+ // nothing for ours to do.
+ EchoCancellerPtr ec = m_echoCanceller;
+ if (ec && (!ec->isValid() || ec->sampleRate() != m_sampleRate))
+ ec.clear();
+
+ SwrContext *swr = nullptr;
+ AVFrame *dec_frame = av_frame_alloc();
+ QVector<qint16> out;
+ bool loggedDecodeError = false;
+ bool loggedResamplerError = false;
+
+ waitForPreroll();
+
+ while (true)
+ {
+ QByteArray pktData;
+ if (!nextPacket(&pktData))
+ break;
+ if (pktData.isEmpty())
+ continue;
+
+ AVPacket *in_pkt = av_packet_alloc();
+ if (av_new_packet(in_pkt, pktData.size()) < 0)
+ {
+ av_packet_free(&in_pkt);
+ continue;
+ }
+ memcpy(in_pkt->data, pktData.constData(), pktData.size());
+
+ int ret = avcodec_send_packet(dec_ctx, in_pkt);
+ av_packet_free(&in_pkt);
+ if (ret < 0)
+ {
+ if (!loggedDecodeError)
+ {
+ qWarning() << QString("Opus decode failed: %1").arg(ff_errstr(ret));
+ loggedDecodeError = true;
+ }
+ continue;
+ }
+
+ while (avcodec_receive_frame(dec_ctx, dec_frame) == 0)
+ {
+ if (!swr)
+ {
+ swr = makeMonoResampler((AVSampleFormat)dec_frame->format, dec_frame->sample_rate,
+ AV_SAMPLE_FMT_S16, m_sampleRate);
+ if (!swr)
+ {
+ if (!loggedResamplerError)
+ {
+ qWarning() << "Could not set up playback resampler";
+ loggedResamplerError = true;
+ }
+ av_frame_unref(dec_frame);
+ continue;
+ }
+ }
+
+ int out_samples = (int)av_rescale_rnd(swr_get_delay(swr, dec_frame->sample_rate) + dec_frame->nb_samples,
+ m_sampleRate, dec_frame->sample_rate, AV_ROUND_UP);
+ if (out_samples > 0)
+ {
+ out.resize(out_samples * kChannels);
+
+ uint8_t *outPlanes[1] = { (uint8_t *)out.data() };
+ int converted = swr_convert(swr, outPlanes, out_samples,
+ (const uint8_t **)dec_frame->data, dec_frame->nb_samples);
+ if (converted > 0)
+ {
+ if (ec)
+ ec->pushFarEnd(out.constData(), converted * kChannels);
+
+ if (backend->write(out.constData(), converted * kChannels, 100) < 0)
+ {
+ qWarning() << "Native playback write failed";
+ av_frame_unref(dec_frame);
+ goto done;
+ }
+ }
+ }
+
+ av_frame_unref(dec_frame);
+ }
+ }
+
+done:
+ av_frame_free(&dec_frame);
+ if (swr) swr_free(&swr);
+ avcodec_free_context(&dec_ctx);
+ backend->close();
+ delete backend;
+}
+
+#ifdef HAVE_LIBPULSE
+
+// PulseAudio's introspection API is asynchronous, so a one-shot enumeration
+// means running a private mainloop until the callbacks below have said they
+// are done. Doing it on the caller's thread with its own context keeps this
+// self contained - nothing else in pica-client talks to PulseAudio.
+struct PulseEnumState
+{
+ QList<MediaDeviceInfo> *result;
+ int *index;
+ // The introspection callback has reported the end of the list, so
+ // whatever happens to the connection afterwards is just teardown.
+ bool completed;
+ bool done;
+ bool failed;
+};
+
+static void pulse_source_cb(pa_context *, const pa_source_info *info, int eol, void *userdata)
+{
+ PulseEnumState *st = (PulseEnumState *)userdata;
+
+ if (eol)
+ {
+ st->completed = true;
+ st->done = true;
+ return;
+ }
+
+ if (!info)
+ return;
+
+ // Monitor sources are the loopback of an output, not a microphone; they
+ // would show up in the capture list as duplicates of every speaker.
+ if (info->monitor_of_sink != PA_INVALID_INDEX)
+ return;
+
+ MediaDeviceInfo d;
+ d.device = QString::fromUtf8(info->name);
+ d.humanReadable = QString::fromUtf8(info->description);
+ d.index = (*st->index)++;
+ *st->result << d;
+}
+
+static void pulse_sink_cb(pa_context *, const pa_sink_info *info, int eol, void *userdata)
+{
+ PulseEnumState *st = (PulseEnumState *)userdata;
+
+ if (eol)
+ {
+ st->completed = true;
+ st->done = true;
+ return;
+ }
+
+ if (!info)
+ return;
+
+ MediaDeviceInfo d;
+ d.device = QString::fromUtf8(info->name);
+ d.humanReadable = QString::fromUtf8(info->description);
+ d.index = (*st->index)++;
+ *st->result << d;
+}
+
+static void pulse_state_cb(pa_context *ctx, void *userdata)
+{
+ PulseEnumState *st = (PulseEnumState *)userdata;
+
+ // READY is handled by the loop in enumeratePulse(), which is where the
+ // introspection request gets issued; only the ways this can end early
+ // need noticing here.
+ switch (pa_context_get_state(ctx))
+ {
+ case PA_CONTEXT_FAILED:
+ case PA_CONTEXT_TERMINATED:
+ if (!st->completed)
+ st->failed = true;
+ st->done = true;
+ break;
+
+ default:
+ break;
+ }
+}
+
+static QList<MediaDeviceInfo> enumeratePulse(enum MediaDeviceStreamDirection dir)
{
QList<MediaDeviceInfo> result;
int index = 0;
+ // PulseAudio's own idea of the default, which follows the user moving
+ // their default device in the desktop's sound settings.
+ {
+ MediaDeviceInfo d;
+ d.device = QStringLiteral("default");
+ d.humanReadable = (dir == PLAYBACK)
+ ? QObject::tr("Default output (PulseAudio)")
+ : QObject::tr("Default input (PulseAudio)");
+ d.index = index++;
+ result << d;
+ }
+
+ pa_mainloop *mainloop = pa_mainloop_new();
+ if (!mainloop)
+ return result;
+
+ pa_context *ctx = pa_context_new(pa_mainloop_get_api(mainloop), "pica-client");
+ if (!ctx)
+ {
+ pa_mainloop_free(mainloop);
+ return result;
+ }
+
+ PulseEnumState st;
+ st.result = &result;
+ st.index = &index;
+ st.completed = false;
+ st.done = false;
+ st.failed = false;
+
+ pa_context_set_state_callback(ctx, pulse_state_cb, &st);
+
+ if (pa_context_connect(ctx, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0)
+ {
+ pa_context_unref(ctx);
+ pa_mainloop_free(mainloop);
+ return result;
+ }
+
+ pa_operation *op = nullptr;
+ bool requested = false;
+
+ // Bounded so a server that never answers cannot hang the settings dialog.
+ // One iteration is at most the 50ms below, so this is a 5 second ceiling.
+ for (int iterations = 0; iterations < 100 && !st.done; iterations++)
+ {
+ if (pa_mainloop_prepare(mainloop, 50 * 1000) < 0)
+ break;
+ if (pa_mainloop_poll(mainloop) < 0)
+ break;
+ if (pa_mainloop_dispatch(mainloop) < 0)
+ break;
+
+ if (!requested && pa_context_get_state(ctx) == PA_CONTEXT_READY)
+ {
+ if (dir == PLAYBACK)
+ op = pa_context_get_sink_info_list(ctx, pulse_sink_cb, &st);
+ else
+ op = pa_context_get_source_info_list(ctx, pulse_source_cb, &st);
+
+ requested = true;
+
+ if (!op)
+ break;
+ }
+ }
+
+ if (op)
+ pa_operation_unref(op);
+
+ pa_context_disconnect(ctx);
+ pa_context_unref(ctx);
+ pa_mainloop_free(mainloop);
+
+ if (st.failed)
+ qWarning() << "Could not enumerate PulseAudio devices - is a sound server running?";
+
+ return result;
+}
+
+#endif // HAVE_LIBPULSE
+
#ifdef Q_OS_LINUX
+
+static QList<MediaDeviceInfo> enumerateAlsa(enum MediaDeviceStreamDirection dir)
+{
+ QList<MediaDeviceInfo> result;
+ int index = 0;
+
void **hints, **n;
char *name = NULL, *descr = NULL, *io = NULL;
const char *direction = (dir == PLAYBACK) ? "Output" : "Input";
@@ -673,19 +1305,94 @@ QList<MediaDeviceInfo> AudioDevice::Enumerate(enum MediaDeviceStreamDirection di
n++;
}
snd_device_name_free_hint(hints);
+
+ return result;
+}
+
+#endif // Q_OS_LINUX
+
+QList<MediaDeviceInfo> AudioDevice::Enumerate(enum MediaDeviceStreamDirection dir)
+{
+ const QString driver = AudioDevice::PlatformDriverName(dir);
+
+ if (AudioDevice::IsPlatformDriver(driver))
+ return NativeAudioBackend::enumerate(AudioDevice::PlatformApiName(driver), dir);
+
+#ifdef HAVE_LIBPULSE
+ if (driver == QLatin1String("pulse"))
+ return enumeratePulse(dir);
+#endif
+
+#ifdef Q_OS_LINUX
+ if (driver == QLatin1String("alsa"))
+ return enumerateAlsa(dir);
#endif
+
+ // Anything else - "oss", or "pulse" in a build without libpulse - has no
+ // enumeration of its own worth writing, so the caller gets the one name
+ // that always works.
+ QList<MediaDeviceInfo> result;
+ MediaDeviceInfo d;
+ d.device = QStringLiteral("default");
+ d.humanReadable = QObject::tr("Default device");
+ d.index = 0;
+ result << d;
+
return result;
}
-QString AudioDevice::PlatformDriverName()
+void AudioDevice::SetLinuxDriverName(const QString &driver)
+{
+ QMutexLocker locker(&s_driverMutex);
+
+ if (driver == QLatin1String("pulse"))
+ s_linuxDriver = QStringLiteral("pulse");
+ else
+ s_linuxDriver = QStringLiteral("alsa");
+}
+
+QString AudioDevice::LinuxDriverName()
{
+ QMutexLocker locker(&s_driverMutex);
+ return s_linuxDriver;
+}
+
+QString AudioDevice::PlatformDriverName(enum MediaDeviceStreamDirection dir)
+{
+ Q_UNUSED(dir)
+
#if defined(Q_OS_LINUX)
- return QStringLiteral("alsa");
+ // Both directions come from the same setting. ALSA is the default because
+ // it is the one interface guaranteed to be there; "pulse" is worth
+ // choosing on a desktop, where it (or PipeWire wearing its face) is what
+ // actually owns the sound card, and where module-echo-cancel can take the
+ // job off our hands entirely.
+ return AudioDevice::LinuxDriverName();
#elif defined(Q_OS_MACOS) || defined(Q_OS_MAC)
- return QStringLiteral("audiotoolbox");
+ // Not libavdevice's "audiotoolbox". CoreAudio's echo cancellation lives
+ // in the VoiceProcessingIO audio unit, which renders and captures
+ // together, and there is no way to reach it through a demuxer and a muxer
+ // that know nothing about each other.
+ return QStringLiteral("platform-coreaudio");
#elif defined(Q_OS_WIN)
- return QStringLiteral("dsound");
+ // DirectSound is a compatibility shim over
+ // WASAPI these days and cannot mark a stream as belonging to a call,
+ // which is the only way to get Windows' own echo cancellation.
+ return QStringLiteral("platform-wasapi");
#else
return QStringLiteral("oss");
#endif
}
+
+bool AudioDevice::IsPlatformDriver(const QString &driver)
+{
+ return driver.startsWith(QLatin1String("platform-"));
+}
+
+QString AudioDevice::PlatformApiName(const QString &driver)
+{
+ if (!IsPlatformDriver(driver))
+ return QString();
+
+ return driver.mid(sizeof("platform-") - 1);
+}