diff options
author | Lars Knoll <lars.knoll@qt.io> | 2020-12-16 11:02:15 +0100 |
---|---|---|
committer | Lars Knoll <lars.knoll@qt.io> | 2021-01-20 15:12:01 +0000 |
commit | f16dbe174cee73308cae2aae512fe837e07e0f55 (patch) | |
tree | d991c5c4fc2f2ca2d9eb48d3fdc4c20a2ef3cffb /src/multimedia/audio | |
parent | a019323edf4f2679f00eb3653e0f8791204cf12a (diff) |
Remove the plugin infrastructure for audio plugins
Move the PulseAudio and ALSA code directly into Qt Multimedia. The
other plugins will follow in the next commits.
Change-Id: I59471fa65639ccc746e6fa182d34373f6f901199
Reviewed-by: Doris Verria <doris.verria@qt.io>
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
Diffstat (limited to 'src/multimedia/audio')
30 files changed, 5827 insertions, 148 deletions
diff --git a/src/multimedia/audio/alsa/alsa.json b/src/multimedia/audio/alsa/alsa.json new file mode 100644 index 000000000..c2b22dfec --- /dev/null +++ b/src/multimedia/audio/alsa/alsa.json @@ -0,0 +1,3 @@ +{ + "Keys": ["alsa"] +} diff --git a/src/multimedia/audio/alsa/alsa.pri b/src/multimedia/audio/alsa/alsa.pri new file mode 100644 index 000000000..b3f5739c0 --- /dev/null +++ b/src/multimedia/audio/alsa/alsa.pri @@ -0,0 +1,11 @@ +QMAKE_USE_PRIVATE += alsa + +HEADERS += audio/alsa/qalsaaudiodeviceinfo_p.h \ + audio/alsa/qalsaaudioinput_p.h \ + audio/alsa/qalsaaudiooutput_p.h \ + audio/alsa/qalsainterface_p.h + +SOURCES += audio/alsa/qalsaaudiodeviceinfo.cpp \ + audio/alsa/qalsaaudioinput.cpp \ + audio/alsa/qalsaaudiooutput.cpp \ + audio/alsa/qalsainterface.cpp diff --git a/src/multimedia/audio/alsa/qalsaaudiodeviceinfo.cpp b/src/multimedia/audio/alsa/qalsaaudiodeviceinfo.cpp new file mode 100644 index 000000000..0d4aa11b5 --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudiodeviceinfo.cpp @@ -0,0 +1,430 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// INTERNAL USE ONLY: Do NOT use for any other purpose. +// + +#include "qalsaaudiodeviceinfo_p.h" + +#include <alsa/version.h> + +QT_BEGIN_NAMESPACE + +QAlsaAudioDeviceInfo::QAlsaAudioDeviceInfo(const QByteArray &dev, QAudio::Mode mode) +{ + handle = 0; + + device = QLatin1String(dev); + this->mode = mode; + + checkSurround(); +} + +QAlsaAudioDeviceInfo::~QAlsaAudioDeviceInfo() +{ + close(); +} + +bool QAlsaAudioDeviceInfo::isFormatSupported(const QAudioFormat& format) const +{ + return testSettings(format); +} + +QAudioFormat QAlsaAudioDeviceInfo::preferredFormat() const +{ + QAudioFormat nearest; + if(mode == QAudio::AudioOutput) { + nearest.setSampleRate(44100); + nearest.setChannelCount(2); + nearest.setByteOrder(QAudioFormat::LittleEndian); + nearest.setSampleType(QAudioFormat::SignedInt); + nearest.setSampleSize(16); + nearest.setCodec(QLatin1String("audio/pcm")); + } else { + nearest.setSampleRate(8000); + nearest.setChannelCount(1); + nearest.setSampleType(QAudioFormat::UnSignedInt); + nearest.setSampleSize(8); + nearest.setCodec(QLatin1String("audio/pcm")); + if(!testSettings(nearest)) { + nearest.setChannelCount(2); + nearest.setSampleSize(16); + nearest.setSampleType(QAudioFormat::SignedInt); + } + } + return nearest; +} + +QString QAlsaAudioDeviceInfo::deviceName() const +{ + return device; +} + +QStringList QAlsaAudioDeviceInfo::supportedCodecs() +{ + updateLists(); + return codecz; +} + +QList<int> QAlsaAudioDeviceInfo::supportedSampleRates() +{ + updateLists(); + return sampleRatez; +} + +QList<int> QAlsaAudioDeviceInfo::supportedChannelCounts() +{ + updateLists(); + return channelz; +} + +QList<int> QAlsaAudioDeviceInfo::supportedSampleSizes() +{ + updateLists(); + return sizez; +} + +QList<QAudioFormat::Endian> QAlsaAudioDeviceInfo::supportedByteOrders() +{ + updateLists(); + return byteOrderz; +} + +QList<QAudioFormat::SampleType> QAlsaAudioDeviceInfo::supportedSampleTypes() +{ + updateLists(); + return typez; +} + +QByteArray QAlsaAudioDeviceInfo::defaultDevice(QAudio::Mode mode) +{ + const auto &devices = availableDevices(mode); + if (devices.size() == 0) + return QByteArray(); + + return devices.first(); +} + +bool QAlsaAudioDeviceInfo::open() +{ + int err = 0; + QString dev; + + if (!availableDevices(mode).contains(device.toLocal8Bit())) + return false; + +#if SND_LIB_VERSION < 0x1000e // 1.0.14 + if (device.compare(QLatin1String("default")) != 0) + dev = deviceFromCardName(device); + else +#endif + dev = device; + + if(mode == QAudio::AudioOutput) { + err=snd_pcm_open( &handle,dev.toLocal8Bit().constData(),SND_PCM_STREAM_PLAYBACK,0); + } else { + err=snd_pcm_open( &handle,dev.toLocal8Bit().constData(),SND_PCM_STREAM_CAPTURE,0); + } + if(err < 0) { + handle = 0; + return false; + } + return true; +} + +void QAlsaAudioDeviceInfo::close() +{ + if(handle) + snd_pcm_close(handle); + handle = 0; +} + +bool QAlsaAudioDeviceInfo::testSettings(const QAudioFormat& format) const +{ + // Set nearest to closest settings that do work. + // See if what is in settings will work (return value). + int err = -1; + snd_pcm_t* pcmHandle; + snd_pcm_hw_params_t *params; + QString dev; + +#if SND_LIB_VERSION < 0x1000e // 1.0.14 + if (device.compare(QLatin1String("default")) != 0) + dev = deviceFromCardName(device); + else +#endif + dev = device; + + snd_pcm_stream_t stream = mode == QAudio::AudioOutput + ? SND_PCM_STREAM_PLAYBACK : SND_PCM_STREAM_CAPTURE; + + if (snd_pcm_open(&pcmHandle, dev.toLocal8Bit().constData(), stream, 0) < 0) + return false; + + snd_pcm_nonblock(pcmHandle, 0); + snd_pcm_hw_params_alloca(¶ms); + snd_pcm_hw_params_any(pcmHandle, params); + + // set the values! + snd_pcm_hw_params_set_channels(pcmHandle, params, format.channelCount()); + snd_pcm_hw_params_set_rate(pcmHandle, params, format.sampleRate(), 0); + + snd_pcm_format_t pcmFormat = SND_PCM_FORMAT_UNKNOWN; + switch (format.sampleSize()) { + case 8: + if (format.sampleType() == QAudioFormat::SignedInt) + pcmFormat = SND_PCM_FORMAT_S8; + else if (format.sampleType() == QAudioFormat::UnSignedInt) + pcmFormat = SND_PCM_FORMAT_U8; + break; + case 16: + if (format.sampleType() == QAudioFormat::SignedInt) { + pcmFormat = format.byteOrder() == QAudioFormat::LittleEndian + ? SND_PCM_FORMAT_S16_LE : SND_PCM_FORMAT_S16_BE; + } else if (format.sampleType() == QAudioFormat::UnSignedInt) { + pcmFormat = format.byteOrder() == QAudioFormat::LittleEndian + ? SND_PCM_FORMAT_U16_LE : SND_PCM_FORMAT_U16_BE; + } + break; + case 32: + if (format.sampleType() == QAudioFormat::SignedInt) { + pcmFormat = format.byteOrder() == QAudioFormat::LittleEndian + ? SND_PCM_FORMAT_S32_LE : SND_PCM_FORMAT_S32_BE; + } else if (format.sampleType() == QAudioFormat::UnSignedInt) { + pcmFormat = format.byteOrder() == QAudioFormat::LittleEndian + ? SND_PCM_FORMAT_U32_LE : SND_PCM_FORMAT_U32_BE; + } else if (format.sampleType() == QAudioFormat::Float) { + pcmFormat = format.byteOrder() == QAudioFormat::LittleEndian + ? SND_PCM_FORMAT_FLOAT_LE : SND_PCM_FORMAT_FLOAT_BE; + } + } + + if (pcmFormat != SND_PCM_FORMAT_UNKNOWN) + err = snd_pcm_hw_params_set_format(pcmHandle, params, pcmFormat); + + // For now, just accept only audio/pcm codec + if (!format.codec().startsWith(QLatin1String("audio/pcm"))) + err = -1; + + if (err >= 0 && format.channelCount() != -1) { + err = snd_pcm_hw_params_test_channels(pcmHandle, params, format.channelCount()); + if (err >= 0) + err = snd_pcm_hw_params_set_channels(pcmHandle, params, format.channelCount()); + } + + if (err >= 0 && format.sampleRate() != -1) { + err = snd_pcm_hw_params_test_rate(pcmHandle, params, format.sampleRate(), 0); + if (err >= 0) + err = snd_pcm_hw_params_set_rate(pcmHandle, params, format.sampleRate(), 0); + } + + if (err >= 0 && pcmFormat != SND_PCM_FORMAT_UNKNOWN) + err = snd_pcm_hw_params_set_format(pcmHandle, params, pcmFormat); + + if (err >= 0) + err = snd_pcm_hw_params(pcmHandle, params); + + snd_pcm_close(pcmHandle); + + return (err == 0); +} + +void QAlsaAudioDeviceInfo::updateLists() +{ + // redo all lists based on current settings + sampleRatez.clear(); + channelz.clear(); + sizez.clear(); + byteOrderz.clear(); + typez.clear(); + codecz.clear(); + + if(!handle) + open(); + + if(!handle) + return; + + for(int i=0; i<(int)MAX_SAMPLE_RATES; i++) { + //if(snd_pcm_hw_params_test_rate(handle, params, SAMPLE_RATES[i], dir) == 0) + sampleRatez.append(SAMPLE_RATES[i]); + } + channelz.append(1); + channelz.append(2); + if (surround40) channelz.append(4); + if (surround51) channelz.append(6); + if (surround71) channelz.append(8); + sizez.append(8); + sizez.append(16); + sizez.append(32); + byteOrderz.append(QAudioFormat::LittleEndian); + byteOrderz.append(QAudioFormat::BigEndian); + typez.append(QAudioFormat::SignedInt); + typez.append(QAudioFormat::UnSignedInt); + typez.append(QAudioFormat::Float); + codecz.append(QLatin1String("audio/pcm")); + close(); +} + +QList<QByteArray> QAlsaAudioDeviceInfo::availableDevices(QAudio::Mode mode) +{ + QList<QByteArray> devices; + bool hasDefault = false; + +#if SND_LIB_VERSION >= 0x1000e // 1.0.14 + QByteArray filter; + + // Create a list of all current audio devices that support mode + void **hints, **n; + char *name, *descr, *io; + + if(snd_device_name_hint(-1, "pcm", &hints) < 0) { + qWarning() << "no alsa devices available"; + return devices; + } + n = hints; + + if(mode == QAudio::AudioInput) { + filter = "Input"; + } else { + filter = "Output"; + } + + while (*n != NULL) { + name = snd_device_name_get_hint(*n, "NAME"); + if (name != 0 && qstrcmp(name, "null") != 0) { + descr = snd_device_name_get_hint(*n, "DESC"); + io = snd_device_name_get_hint(*n, "IOID"); + + if ((descr != NULL) && ((io == NULL) || (io == filter))) { + devices.append(name); + if (strcmp(name, "default") == 0) + hasDefault = true; + } + + free(descr); + free(io); + } + free(name); + ++n; + } + snd_device_name_free_hint(hints); +#else + int idx = 0; + char* name; + + while(snd_card_get_name(idx,&name) == 0) { + devices.append(name); + if (strcmp(name, "default") == 0) + hasDefault = true; + idx++; + } +#endif + + if (!hasDefault && devices.size() > 0) + devices.prepend("default"); + + return devices; +} + +void QAlsaAudioDeviceInfo::checkSurround() +{ + surround40 = false; + surround51 = false; + surround71 = false; + + void **hints, **n; + char *name, *descr, *io; + + if(snd_device_name_hint(-1, "pcm", &hints) < 0) + return; + + n = hints; + + while (*n != NULL) { + name = snd_device_name_get_hint(*n, "NAME"); + descr = snd_device_name_get_hint(*n, "DESC"); + io = snd_device_name_get_hint(*n, "IOID"); + if((name != NULL) && (descr != NULL)) { + QString deviceName = QLatin1String(name); + if (mode == QAudio::AudioOutput) { + if(deviceName.contains(QLatin1String("surround40"))) + surround40 = true; + if(deviceName.contains(QLatin1String("surround51"))) + surround51 = true; + if(deviceName.contains(QLatin1String("surround71"))) + surround71 = true; + } + } + if(name != NULL) + free(name); + if(descr != NULL) + free(descr); + if(io != NULL) + free(io); + ++n; + } + snd_device_name_free_hint(hints); +} + +QString QAlsaAudioDeviceInfo::deviceFromCardName(const QString &card) +{ + int idx = 0; + char *name; + + QStringView shortName = QStringView{card}.mid(card.indexOf(QLatin1String("="), 0) + 1); + + while (snd_card_get_name(idx, &name) == 0) { + if (shortName.compare(QLatin1String(name)) == 0) + break; + idx++; + } + + return QString(QLatin1String("hw:%1,0")).arg(idx); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/alsa/qalsaaudiodeviceinfo_p.h b/src/multimedia/audio/alsa/qalsaaudiodeviceinfo_p.h new file mode 100644 index 000000000..cdf08bfab --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudiodeviceinfo_p.h @@ -0,0 +1,120 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#ifndef QALSAAUDIODEVICEINFO_H +#define QALSAAUDIODEVICEINFO_H + +#include <alsa/asoundlib.h> + +#include <QtCore/qbytearray.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qlist.h> +#include <QtCore/qdebug.h> + +#include <QtMultimedia/qaudio.h> +#include <QtMultimedia/qaudiodeviceinfo.h> +#include <QtMultimedia/qaudiosystem.h> + +QT_BEGIN_NAMESPACE + + +const unsigned int MAX_SAMPLE_RATES = 5; +const unsigned int SAMPLE_RATES[] = + { 8000, 11025, 22050, 44100, 48000 }; + +class QAlsaAudioDeviceInfo : public QAbstractAudioDeviceInfo +{ + Q_OBJECT +public: + QAlsaAudioDeviceInfo(const QByteArray &dev,QAudio::Mode mode); + ~QAlsaAudioDeviceInfo(); + + bool testSettings(const QAudioFormat& format) const; + void updateLists(); + QAudioFormat preferredFormat() const override; + bool isFormatSupported(const QAudioFormat& format) const override; + QString deviceName() const override; + QStringList supportedCodecs() override; + QList<int> supportedSampleRates() override; + QList<int> supportedChannelCounts() override; + QList<int> supportedSampleSizes() override; + QList<QAudioFormat::Endian> supportedByteOrders() override; + QList<QAudioFormat::SampleType> supportedSampleTypes() override; + static QByteArray defaultDevice(QAudio::Mode mode); + static QList<QByteArray> availableDevices(QAudio::Mode); + static QString deviceFromCardName(const QString &card); + +private: + bool open(); + void close(); + + void checkSurround(); + bool surround40; + bool surround51; + bool surround71; + + QString device; + QAudio::Mode mode; + QAudioFormat nearest; + QList<int> sampleRatez; + QList<int> channelz; + QList<int> sizez; + QList<QAudioFormat::Endian> byteOrderz; + QStringList codecz; + QList<QAudioFormat::SampleType> typez; + snd_pcm_t* handle; + snd_pcm_hw_params_t *params; +}; + +QT_END_NAMESPACE + + +#endif // QALSAAUDIODEVICEINFO_H diff --git a/src/multimedia/audio/alsa/qalsaaudioinput.cpp b/src/multimedia/audio/alsa/qalsaaudioinput.cpp new file mode 100644 index 000000000..2e598c5bf --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudioinput.cpp @@ -0,0 +1,872 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// INTERNAL USE ONLY: Do NOT use for any other purpose. +// + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qvarlengtharray.h> +#include <QtMultimedia/private/qaudiohelpers_p.h> +#include "qalsaaudioinput_p.h" +#include "qalsaaudiodeviceinfo_p.h" + +QT_BEGIN_NAMESPACE + +//#define DEBUG_AUDIO 1 + +QAlsaAudioInput::QAlsaAudioInput(const QByteArray &device) +{ + bytesAvailable = 0; + handle = 0; + access = SND_PCM_ACCESS_RW_INTERLEAVED; + pcmformat = SND_PCM_FORMAT_S16; + buffer_size = 0; + period_size = 0; + buffer_time = 100000; + period_time = 20000; + totalTimeValue = 0; + intervalTime = 1000; + errorState = QAudio::NoError; + deviceState = QAudio::StoppedState; + audioSource = 0; + pullMode = true; + resuming = false; + + m_volume = 1.0f; + + m_device = device; + + timer = new QTimer(this); + connect(timer,SIGNAL(timeout()),SLOT(userFeed())); +} + +QAlsaAudioInput::~QAlsaAudioInput() +{ + close(); + disconnect(timer, SIGNAL(timeout())); + QCoreApplication::processEvents(); + delete timer; +} + +void QAlsaAudioInput::setVolume(qreal vol) +{ + m_volume = vol; +} + +qreal QAlsaAudioInput::volume() const +{ + return m_volume; +} + +QAudio::Error QAlsaAudioInput::error() const +{ + return errorState; +} + +QAudio::State QAlsaAudioInput::state() const +{ + return deviceState; +} + +void QAlsaAudioInput::setFormat(const QAudioFormat& fmt) +{ + if (deviceState == QAudio::StoppedState) + settings = fmt; +} + +QAudioFormat QAlsaAudioInput::format() const +{ + return settings; +} + +int QAlsaAudioInput::xrun_recovery(int err) +{ + int count = 0; + bool reset = false; + + // ESTRPIPE is not available in all OSes where ALSA is available + int estrpipe = EIO; +#ifdef ESTRPIPE + estrpipe = ESTRPIPE; +#endif + + if(err == -EPIPE) { + errorState = QAudio::UnderrunError; + err = snd_pcm_prepare(handle); + if(err < 0) + reset = true; + else { + bytesAvailable = checkBytesReady(); + if (bytesAvailable <= 0) + reset = true; + } + } else if ((err == -estrpipe)||(err == -EIO)) { + errorState = QAudio::IOError; + while((err = snd_pcm_resume(handle)) == -EAGAIN){ + usleep(100); + count++; + if(count > 5) { + reset = true; + break; + } + } + if(err < 0) { + err = snd_pcm_prepare(handle); + if(err < 0) + reset = true; + } + } + if(reset) { + close(); + open(); + snd_pcm_prepare(handle); + return 0; + } + return err; +} + +int QAlsaAudioInput::setFormat() +{ + snd_pcm_format_t format = SND_PCM_FORMAT_UNKNOWN; + + if(settings.sampleSize() == 8) { + format = SND_PCM_FORMAT_U8; + } else if(settings.sampleSize() == 16) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_S16_LE; + else + format = SND_PCM_FORMAT_S16_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_U16_LE; + else + format = SND_PCM_FORMAT_U16_BE; + } + } else if(settings.sampleSize() == 24) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_S24_LE; + else + format = SND_PCM_FORMAT_S24_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_U24_LE; + else + format = SND_PCM_FORMAT_U24_BE; + } + } else if(settings.sampleSize() == 32) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_S32_LE; + else + format = SND_PCM_FORMAT_S32_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_U32_LE; + else + format = SND_PCM_FORMAT_U32_BE; + } else if(settings.sampleType() == QAudioFormat::Float) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_FLOAT_LE; + else + format = SND_PCM_FORMAT_FLOAT_BE; + } + } else if(settings.sampleSize() == 64) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + format = SND_PCM_FORMAT_FLOAT64_LE; + else + format = SND_PCM_FORMAT_FLOAT64_BE; + } + + return format != SND_PCM_FORMAT_UNKNOWN + ? snd_pcm_hw_params_set_format( handle, hwparams, format) + : -1; +} + +void QAlsaAudioInput::start(QIODevice* device) +{ + if(deviceState != QAudio::StoppedState) + close(); + + if(!pullMode && audioSource) + delete audioSource; + + pullMode = true; + audioSource = device; + + deviceState = QAudio::ActiveState; + + if( !open() ) + return; + + emit stateChanged(deviceState); +} + +QIODevice* QAlsaAudioInput::start() +{ + if(deviceState != QAudio::StoppedState) + close(); + + if(!pullMode && audioSource) + delete audioSource; + + pullMode = false; + audioSource = new AlsaInputPrivate(this); + audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + + deviceState = QAudio::IdleState; + + if( !open() ) + return 0; + + emit stateChanged(deviceState); + + return audioSource; +} + +void QAlsaAudioInput::stop() +{ + if(deviceState == QAudio::StoppedState) + return; + + deviceState = QAudio::StoppedState; + + close(); + emit stateChanged(deviceState); +} + +bool QAlsaAudioInput::open() +{ +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; +#endif + clockStamp.restart(); + timeStamp.restart(); + elapsedTimeOffset = 0; + + int dir; + int err = 0; + int count=0; + unsigned int sampleRate=settings.sampleRate(); + + if (!settings.isValid()) { + qWarning("QAudioInput: open error, invalid format."); + } else if (settings.sampleRate() <= 0) { + qWarning("QAudioInput: open error, invalid sample rate (%d).", + settings.sampleRate()); + } else { + err = -1; + } + + if (err == 0) { + errorState = QAudio::OpenError; + deviceState = QAudio::StoppedState; + emit errorChanged(errorState); + return false; + } + + + if (!QAlsaAudioDeviceInfo::availableDevices(QAudio::AudioInput).contains(m_device)) + return false; + + QString dev; +#if SND_LIB_VERSION < 0x1000e // 1.0.14 + if (m_device != "default") + dev = QAlsaAudioDeviceInfo::deviceFromCardName(m_device); + else +#endif + dev = m_device; + + // Step 1: try and open the device + while((count < 5) && (err < 0)) { + err=snd_pcm_open(&handle,dev.toLocal8Bit().constData(),SND_PCM_STREAM_CAPTURE,0); + if(err < 0) + count++; + } + if (( err < 0)||(handle == 0)) { + errorState = QAudio::OpenError; + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + return false; + } + snd_pcm_nonblock( handle, 0 ); + + // Step 2: Set the desired HW parameters. + snd_pcm_hw_params_alloca( &hwparams ); + + bool fatal = false; + QString errMessage; + unsigned int chunks = 8; + + err = snd_pcm_hw_params_any( handle, hwparams ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_any: err = %1").arg(err); + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_rate_resample( handle, hwparams, 1 ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_rate_resample: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_access( handle, hwparams, access ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_access: err = %1").arg(err); + } + } + if ( !fatal ) { + err = setFormat(); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_format: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_channels( handle, hwparams, (unsigned int)settings.channelCount() ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_channels: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_rate_near( handle, hwparams, &sampleRate, 0 ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_rate_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_buffer_time_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_period_time_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params_set_periods_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params(handle, hwparams); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioInput: snd_pcm_hw_params: err = %1").arg(err); + } + } + if( err < 0) { + qWarning()<<errMessage; + errorState = QAudio::OpenError; + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + return false; + } + snd_pcm_hw_params_get_buffer_size(hwparams,&buffer_frames); + buffer_size = snd_pcm_frames_to_bytes(handle,buffer_frames); + snd_pcm_hw_params_get_period_size(hwparams,&period_frames, &dir); + period_size = snd_pcm_frames_to_bytes(handle,period_frames); + snd_pcm_hw_params_get_buffer_time(hwparams,&buffer_time, &dir); + snd_pcm_hw_params_get_period_time(hwparams,&period_time, &dir); + + // Step 3: Set the desired SW parameters. + snd_pcm_sw_params_t *swparams; + snd_pcm_sw_params_alloca(&swparams); + snd_pcm_sw_params_current(handle, swparams); + snd_pcm_sw_params_set_start_threshold(handle,swparams,period_frames); + snd_pcm_sw_params_set_stop_threshold(handle,swparams,buffer_frames); + snd_pcm_sw_params_set_avail_min(handle, swparams,period_frames); + snd_pcm_sw_params(handle, swparams); + + // Step 4: Prepare audio + ringBuffer.resize(buffer_size); + snd_pcm_prepare( handle ); + snd_pcm_start(handle); + + // Step 5: Setup timer + bytesAvailable = checkBytesReady(); + + if(pullMode) + connect(audioSource,SIGNAL(readyRead()),this,SLOT(userFeed())); + + // Step 6: Start audio processing + chunks = buffer_size/period_size; + timer->start(period_time*chunks/2000); + + errorState = QAudio::NoError; + + totalTimeValue = 0; + + return true; +} + +void QAlsaAudioInput::close() +{ + timer->stop(); + + if ( handle ) { + snd_pcm_drop( handle ); + snd_pcm_close( handle ); + handle = 0; + } +} + +int QAlsaAudioInput::checkBytesReady() +{ + if(resuming) + bytesAvailable = period_size; + else if(deviceState != QAudio::ActiveState + && deviceState != QAudio::IdleState) + bytesAvailable = 0; + else { + int frames = snd_pcm_avail_update(handle); + if (frames < 0) { + bytesAvailable = frames; + } else { + if((int)frames > (int)buffer_frames) + frames = buffer_frames; + bytesAvailable = snd_pcm_frames_to_bytes(handle, frames); + } + } + return bytesAvailable; +} + +int QAlsaAudioInput::bytesReady() const +{ + return qMax(bytesAvailable, 0); +} + +qint64 QAlsaAudioInput::read(char* data, qint64 len) +{ + // Read in some audio data and write it to QIODevice, pull mode + if ( !handle ) + return 0; + + int bytesRead = 0; + int bytesInRingbufferBeforeRead = ringBuffer.bytesOfDataInBuffer(); + + if (ringBuffer.bytesOfDataInBuffer() < len) { + + // bytesAvaiable is saved as a side effect of checkBytesReady(). + int bytesToRead = checkBytesReady(); + + if (bytesToRead < 0) { + // bytesAvailable as negative is error code, try to recover from it. + xrun_recovery(bytesToRead); + bytesToRead = checkBytesReady(); + if (bytesToRead < 0) { + // recovery failed must stop and set error. + close(); + errorState = QAudio::IOError; + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + return 0; + } + } + + bytesToRead = qMin<qint64>(len, bytesToRead); + bytesToRead = qMin<qint64>(ringBuffer.freeBytes(), bytesToRead); + bytesToRead -= bytesToRead % period_size; + + int count=0; + int err = 0; + QVarLengthArray<char, 4096> buffer(bytesToRead); + while(count < 5 && bytesToRead > 0) { + int chunks = bytesToRead / period_size; + int frames = chunks * period_frames; + if (frames > (int)buffer_frames) + frames = buffer_frames; + + int readFrames = snd_pcm_readi(handle, buffer.data(), frames); + bytesRead = snd_pcm_frames_to_bytes(handle, readFrames); + if (m_volume < 1.0f) + QAudioHelperInternal::qMultiplySamples(m_volume, settings, + buffer.constData(), + buffer.data(), bytesRead); + + if (readFrames >= 0) { + ringBuffer.write(buffer.data(), bytesRead); +#ifdef DEBUG_AUDIO + qDebug() << QString::fromLatin1("read in bytes = %1 (frames=%2)").arg(bytesRead).arg(readFrames).toLatin1().constData(); +#endif + break; + } else if((readFrames == -EAGAIN) || (readFrames == -EINTR)) { + errorState = QAudio::IOError; + err = 0; + break; + } else { + if(readFrames == -EPIPE) { + errorState = QAudio::UnderrunError; + err = snd_pcm_prepare(handle); +#ifdef ESTRPIPE + } else if(readFrames == -ESTRPIPE) { + err = snd_pcm_prepare(handle); +#endif + } + if(err != 0) break; + } + count++; + } + + } + + bytesRead += bytesInRingbufferBeforeRead; + + if (bytesRead > 0) { + // got some send it onward +#ifdef DEBUG_AUDIO + qDebug() << "frames to write to QIODevice = " << + snd_pcm_bytes_to_frames( handle, (int)bytesRead ) << " (" << bytesRead << ") bytes"; +#endif + if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState) + return 0; + + if (pullMode) { + qint64 l = 0; + qint64 bytesWritten = 0; + while (ringBuffer.bytesOfDataInBuffer() > 0) { + l = audioSource->write(ringBuffer.availableData(), ringBuffer.availableDataBlockSize()); + if (l > 0) { + ringBuffer.readBytes(l); + bytesWritten += l; + } else { + break; + } + } + + if (l < 0) { + close(); + errorState = QAudio::IOError; + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + } else if (l == 0 && bytesWritten == 0) { + if (deviceState != QAudio::IdleState) { + errorState = QAudio::NoError; + deviceState = QAudio::IdleState; + emit stateChanged(deviceState); + } + } else { + bytesAvailable -= bytesWritten; + totalTimeValue += bytesWritten; + resuming = false; + if (deviceState != QAudio::ActiveState) { + errorState = QAudio::NoError; + deviceState = QAudio::ActiveState; + emit stateChanged(deviceState); + } + } + + return bytesWritten; + } else { + while (ringBuffer.bytesOfDataInBuffer() > 0) { + int size = ringBuffer.availableDataBlockSize(); + memcpy(data, ringBuffer.availableData(), size); + data += size; + ringBuffer.readBytes(size); + } + + bytesAvailable -= bytesRead; + totalTimeValue += bytesRead; + resuming = false; + if (deviceState != QAudio::ActiveState) { + errorState = QAudio::NoError; + deviceState = QAudio::ActiveState; + emit stateChanged(deviceState); + } + + return bytesRead; + } + } + + return 0; +} + +void QAlsaAudioInput::resume() +{ + if(deviceState == QAudio::SuspendedState) { + int err = 0; + + if(handle) { + err = snd_pcm_prepare( handle ); + if(err < 0) + xrun_recovery(err); + + err = snd_pcm_start(handle); + if(err < 0) + xrun_recovery(err); + + bytesAvailable = buffer_size; + } + resuming = true; + deviceState = QAudio::ActiveState; + int chunks = buffer_size/period_size; + timer->start(period_time*chunks/2000); + emit stateChanged(deviceState); + } +} + +void QAlsaAudioInput::setBufferSize(int value) +{ + buffer_size = value; +} + +int QAlsaAudioInput::bufferSize() const +{ + return buffer_size; +} + +int QAlsaAudioInput::periodSize() const +{ + return period_size; +} + +void QAlsaAudioInput::setNotifyInterval(int ms) +{ + intervalTime = qMax(0, ms); +} + +int QAlsaAudioInput::notifyInterval() const +{ + return intervalTime; +} + +qint64 QAlsaAudioInput::processedUSecs() const +{ + qint64 result = qint64(1000000) * totalTimeValue / + (settings.channelCount()*(settings.sampleSize()/8)) / + settings.sampleRate(); + + return result; +} + +void QAlsaAudioInput::suspend() +{ + if(deviceState == QAudio::ActiveState||resuming) { + snd_pcm_drain(handle); + timer->stop(); + deviceState = QAudio::SuspendedState; + emit stateChanged(deviceState); + } +} + +void QAlsaAudioInput::userFeed() +{ + if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState) + return; +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :userFeed() IN"; +#endif + deviceReady(); +} + +bool QAlsaAudioInput::deviceReady() +{ + if(pullMode) { + // reads some audio data and writes it to QIODevice + read(0, buffer_size); + } else { + // emits readyRead() so user will call read() on QIODevice to get some audio data + AlsaInputPrivate* a = qobject_cast<AlsaInputPrivate*>(audioSource); + a->trigger(); + } + bytesAvailable = checkBytesReady(); + + if(deviceState != QAudio::ActiveState) + return true; + + if (bytesAvailable < 0) { + // bytesAvailable as negative is error code, try to recover from it. + xrun_recovery(bytesAvailable); + bytesAvailable = checkBytesReady(); + if (bytesAvailable < 0) { + // recovery failed must stop and set error. + close(); + errorState = QAudio::IOError; + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + return 0; + } + } + + if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime) { + emit notify(); + elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime; + timeStamp.restart(); + } + return true; +} + +qint64 QAlsaAudioInput::elapsedUSecs() const +{ + if (deviceState == QAudio::StoppedState) + return 0; + + return clockStamp.elapsed() * qint64(1000); +} + +void QAlsaAudioInput::reset() +{ + if(handle) + snd_pcm_reset(handle); + stop(); + bytesAvailable = 0; +} + +void QAlsaAudioInput::drain() +{ + if(handle) + snd_pcm_drain(handle); +} + +AlsaInputPrivate::AlsaInputPrivate(QAlsaAudioInput* audio) +{ + audioDevice = qobject_cast<QAlsaAudioInput*>(audio); +} + +AlsaInputPrivate::~AlsaInputPrivate() +{ +} + +qint64 AlsaInputPrivate::readData( char* data, qint64 len) +{ + return audioDevice->read(data,len); +} + +qint64 AlsaInputPrivate::writeData(const char* data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +void AlsaInputPrivate::trigger() +{ + emit readyRead(); +} + +RingBuffer::RingBuffer() : + m_head(0), + m_tail(0) +{ +} + +void RingBuffer::resize(int size) +{ + m_data.resize(size); +} + +int RingBuffer::bytesOfDataInBuffer() const +{ + if (m_head < m_tail) + return m_tail - m_head; + else if (m_tail < m_head) + return m_data.size() + m_tail - m_head; + else + return 0; +} + +int RingBuffer::freeBytes() const +{ + if (m_head > m_tail) + return m_head - m_tail - 1; + else if (m_tail > m_head) + return m_data.size() - m_tail + m_head - 1; + else + return m_data.size() - 1; +} + +const char *RingBuffer::availableData() const +{ + return (m_data.constData() + m_head); +} + +int RingBuffer::availableDataBlockSize() const +{ + if (m_head > m_tail) + return m_data.size() - m_head; + else if (m_tail > m_head) + return m_tail - m_head; + else + return 0; +} + +void RingBuffer::readBytes(int bytes) +{ + m_head = (m_head + bytes) % m_data.size(); +} + +void RingBuffer::write(char *data, int len) +{ + if (m_tail + len < m_data.size()) { + memcpy(m_data.data() + m_tail, data, len); + m_tail += len; + } else { + int bytesUntilEnd = m_data.size() - m_tail; + memcpy(m_data.data() + m_tail, data, bytesUntilEnd); + if (len - bytesUntilEnd > 0) + memcpy(m_data.data(), data + bytesUntilEnd, len - bytesUntilEnd); + m_tail = len - bytesUntilEnd; + } +} + +QT_END_NAMESPACE + +#include "moc_qalsaaudioinput_p.cpp" diff --git a/src/multimedia/audio/alsa/qalsaaudioinput_p.h b/src/multimedia/audio/alsa/qalsaaudioinput_p.h new file mode 100644 index 000000000..62e1be039 --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudioinput_p.h @@ -0,0 +1,185 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#ifndef QAUDIOINPUTALSA_H +#define QAUDIOINPUTALSA_H + +#include <alsa/asoundlib.h> + +#include <QtCore/qfile.h> +#include <QtCore/qdebug.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include <QtMultimedia/qaudio.h> +#include <QtMultimedia/qaudiodeviceinfo.h> +#include <QtMultimedia/qaudiosystem.h> + +QT_BEGIN_NAMESPACE + + +class AlsaInputPrivate; + +class RingBuffer +{ +public: + RingBuffer(); + + void resize(int size); + + int bytesOfDataInBuffer() const; + int freeBytes() const; + + const char *availableData() const; + int availableDataBlockSize() const; + void readBytes(int bytes); + + void write(char *data, int len); + +private: + int m_head; + int m_tail; + + QByteArray m_data; +}; + +class QAlsaAudioInput : public QAbstractAudioInput +{ + Q_OBJECT +public: + QAlsaAudioInput(const QByteArray &device); + ~QAlsaAudioInput(); + + qint64 read(char* data, qint64 len); + + void start(QIODevice* device) override; + QIODevice* start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + int bytesReady() const override; + int periodSize() const override; + void setBufferSize(int value) override; + int bufferSize() const override; + void setNotifyInterval(int milliSeconds) override; + int notifyInterval() const override; + qint64 processedUSecs() const override; + qint64 elapsedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat& fmt) override; + QAudioFormat format() const override; + void setVolume(qreal) override; + qreal volume() const override; + bool resuming; + snd_pcm_t* handle; + qint64 totalTimeValue; + QIODevice* audioSource; + QAudioFormat settings; + QAudio::Error errorState; + QAudio::State deviceState; + +private slots: + void userFeed(); + bool deviceReady(); + +private: + int checkBytesReady(); + int xrun_recovery(int err); + int setFormat(); + bool open(); + void close(); + void drain(); + + QTimer* timer; + QElapsedTimer timeStamp; + QElapsedTimer clockStamp; + qint64 elapsedTimeOffset; + int intervalTime; + RingBuffer ringBuffer; + int bytesAvailable; + QByteArray m_device; + bool pullMode; + int buffer_size; + int period_size; + unsigned int buffer_time; + unsigned int period_time; + snd_pcm_uframes_t buffer_frames; + snd_pcm_uframes_t period_frames; + snd_pcm_access_t access; + snd_pcm_format_t pcmformat; + snd_pcm_hw_params_t *hwparams; + qreal m_volume; +}; + +class AlsaInputPrivate : public QIODevice +{ + Q_OBJECT +public: + AlsaInputPrivate(QAlsaAudioInput* audio); + ~AlsaInputPrivate(); + + qint64 readData( char* data, qint64 len) override; + qint64 writeData(const char* data, qint64 len) override; + + void trigger(); +private: + QAlsaAudioInput *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/multimedia/audio/alsa/qalsaaudiooutput.cpp b/src/multimedia/audio/alsa/qalsaaudiooutput.cpp new file mode 100644 index 000000000..a6e80637c --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudiooutput.cpp @@ -0,0 +1,813 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// INTERNAL USE ONLY: Do NOT use for any other purpose. +// + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qvarlengtharray.h> +#include <QtMultimedia/private/qaudiohelpers_p.h> +#include "qalsaaudiooutput_p.h" +#include "qalsaaudiodeviceinfo_p.h" +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcAlsaOutput, "qt.multimedia.alsa.output") +//#define DEBUG_AUDIO 1 + +QAlsaAudioOutput::QAlsaAudioOutput(const QByteArray &device) +{ + bytesAvailable = 0; + handle = 0; + access = SND_PCM_ACCESS_RW_INTERLEAVED; + pcmformat = SND_PCM_FORMAT_S16; + buffer_frames = 0; + period_frames = 0; + buffer_size = 0; + period_size = 0; + buffer_time = 100000; + period_time = 20000; + totalTimeValue = 0; + intervalTime = 1000; + audioBuffer = 0; + errorState = QAudio::NoError; + deviceState = QAudio::StoppedState; + audioSource = 0; + pullMode = true; + resuming = false; + opened = false; + + m_volume = 1.0f; + + m_device = device; + + timer = new QTimer(this); + connect(timer,SIGNAL(timeout()),SLOT(userFeed())); +} + +QAlsaAudioOutput::~QAlsaAudioOutput() +{ + close(); + disconnect(timer, SIGNAL(timeout())); + QCoreApplication::processEvents(); + delete timer; +} + +void QAlsaAudioOutput::setVolume(qreal vol) +{ + m_volume = vol; +} + +qreal QAlsaAudioOutput::volume() const +{ + return m_volume; +} + +QAudio::Error QAlsaAudioOutput::error() const +{ + return errorState; +} + +QAudio::State QAlsaAudioOutput::state() const +{ + return deviceState; +} + +int QAlsaAudioOutput::xrun_recovery(int err) +{ + int count = 0; + bool reset = false; + + // ESTRPIPE is not available in all OSes where ALSA is available + int estrpipe = EIO; +#ifdef ESTRPIPE + estrpipe = ESTRPIPE; +#endif + + if(err == -EPIPE) { + errorState = QAudio::UnderrunError; + emit errorChanged(errorState); + err = snd_pcm_prepare(handle); + if(err < 0) + reset = true; + + } else if ((err == -estrpipe)||(err == -EIO)) { + errorState = QAudio::IOError; + emit errorChanged(errorState); + while((err = snd_pcm_resume(handle)) == -EAGAIN){ + usleep(100); + count++; + if(count > 5) { + reset = true; + break; + } + } + if(err < 0) { + err = snd_pcm_prepare(handle); + if(err < 0) + reset = true; + } + } + if(reset) { + close(); + open(); + snd_pcm_prepare(handle); + return 0; + } + return err; +} + +int QAlsaAudioOutput::setFormat() +{ + snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN; + + if(settings.sampleSize() == 8) { + pcmformat = SND_PCM_FORMAT_U8; + + } else if(settings.sampleSize() == 16) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_S16_LE; + else + pcmformat = SND_PCM_FORMAT_S16_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_U16_LE; + else + pcmformat = SND_PCM_FORMAT_U16_BE; + } + } else if(settings.sampleSize() == 24) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_S24_LE; + else + pcmformat = SND_PCM_FORMAT_S24_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_U24_LE; + else + pcmformat = SND_PCM_FORMAT_U24_BE; + } + } else if(settings.sampleSize() == 32) { + if(settings.sampleType() == QAudioFormat::SignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_S32_LE; + else + pcmformat = SND_PCM_FORMAT_S32_BE; + } else if(settings.sampleType() == QAudioFormat::UnSignedInt) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_U32_LE; + else + pcmformat = SND_PCM_FORMAT_U32_BE; + } else if(settings.sampleType() == QAudioFormat::Float) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_FLOAT_LE; + else + pcmformat = SND_PCM_FORMAT_FLOAT_BE; + } + } else if(settings.sampleSize() == 64) { + if(settings.byteOrder() == QAudioFormat::LittleEndian) + pcmformat = SND_PCM_FORMAT_FLOAT64_LE; + else + pcmformat = SND_PCM_FORMAT_FLOAT64_BE; + } + + return pcmformat != SND_PCM_FORMAT_UNKNOWN + ? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat) + : -1; +} + +void QAlsaAudioOutput::start(QIODevice* device) +{ + if(deviceState != QAudio::StoppedState) + deviceState = QAudio::StoppedState; + + errorState = QAudio::NoError; + + // Handle change of mode + if(audioSource && !pullMode) { + delete audioSource; + audioSource = 0; + } + + close(); + + pullMode = true; + audioSource = device; + + deviceState = QAudio::ActiveState; + + open(); + + emit stateChanged(deviceState); +} + +QIODevice* QAlsaAudioOutput::start() +{ + if(deviceState != QAudio::StoppedState) + deviceState = QAudio::StoppedState; + + errorState = QAudio::NoError; + + // Handle change of mode + if(audioSource && !pullMode) { + delete audioSource; + audioSource = 0; + } + + close(); + + audioSource = new AlsaOutputPrivate(this); + audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + pullMode = false; + + deviceState = QAudio::IdleState; + + open(); + + emit stateChanged(deviceState); + + return audioSource; +} + +void QAlsaAudioOutput::stop() +{ + if(deviceState == QAudio::StoppedState) + return; + errorState = QAudio::NoError; + deviceState = QAudio::StoppedState; + close(); + emit stateChanged(deviceState); +} + +bool QAlsaAudioOutput::open() +{ + if(opened) + return true; + +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; +#endif + timeStamp.restart(); + elapsedTimeOffset = 0; + + int dir; + int err = 0; + int count=0; + unsigned int sampleRate=settings.sampleRate(); + + if (!settings.isValid()) { + qWarning("QAudioOutput: open error, invalid format."); + } else if (settings.sampleRate() <= 0) { + qWarning("QAudioOutput: open error, invalid sample rate (%d).", + settings.sampleRate()); + } else { + err = -1; + } + + if (err == 0) { + errorState = QAudio::OpenError; + deviceState = QAudio::StoppedState; + emit errorChanged(errorState); + return false; + } + + QString dev; +#if SND_LIB_VERSION < 0x1000e // 1.0.14 + if (m_device != "default") + dev = QAlsaAudioDeviceInfo::deviceFromCardName(m_device); + else +#endif + dev = m_device; + + // Step 1: try and open the device + while((count < 5) && (err < 0)) { + err=snd_pcm_open(&handle,dev.toLocal8Bit().constData(),SND_PCM_STREAM_PLAYBACK,0); + if(err < 0) + count++; + } + if (( err < 0)||(handle == 0)) { + errorState = QAudio::OpenError; + emit errorChanged(errorState); + deviceState = QAudio::StoppedState; + return false; + } + snd_pcm_nonblock( handle, 0 ); + + // Step 2: Set the desired HW parameters. + snd_pcm_hw_params_alloca( &hwparams ); + + bool fatal = false; + QString errMessage; + unsigned int chunks = 8; + + err = snd_pcm_hw_params_any( handle, hwparams ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_any: err = %1").arg(err); + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_rate_resample( handle, hwparams, 1 ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_rate_resample: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_access( handle, hwparams, access ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_access: err = %1").arg(err); + } + } + if ( !fatal ) { + err = setFormat(); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_format: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_channels( handle, hwparams, (unsigned int)settings.channelCount() ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_channels: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_rate_near( handle, hwparams, &sampleRate, 0 ); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_rate_near: err = %1").arg(err); + } + } + if ( !fatal ) { + unsigned int maxBufferTime = 0; + unsigned int minBufferTime = 0; + unsigned int maxPeriodTime = 0; + unsigned int minPeriodTime = 0; + + err = snd_pcm_hw_params_get_buffer_time_max(hwparams, &maxBufferTime, &dir); + if ( err >= 0) + err = snd_pcm_hw_params_get_buffer_time_min(hwparams, &minBufferTime, &dir); + if ( err >= 0) + err = snd_pcm_hw_params_get_period_time_max(hwparams, &maxPeriodTime, &dir); + if ( err >= 0) + err = snd_pcm_hw_params_get_period_time_min(hwparams, &minPeriodTime, &dir); + + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: buffer/period min and max: err = %1").arg(err); + } else { + static unsigned user_buffer_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_BUFFER_TIME"); + static unsigned user_period_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_PERIOD_TIME"); + const bool outOfRange = maxBufferTime < buffer_time || buffer_time < minBufferTime || maxPeriodTime < period_time || minPeriodTime > period_time; + if (outOfRange || user_period_time || user_buffer_time) { + period_time = user_period_time ? user_period_time : minPeriodTime; + if (!user_buffer_time) { + chunks = maxBufferTime / period_time; + buffer_time = period_time * chunks; + } else { + buffer_time = user_buffer_time; + chunks = buffer_time / period_time; + } + } + qCDebug(lcAlsaOutput) << "buffer time: [" << minBufferTime << "-" << maxBufferTime << "] =" << buffer_time; + qCDebug(lcAlsaOutput) << "period time: [" << minPeriodTime << "-" << maxPeriodTime << "] =" << period_time; + qCDebug(lcAlsaOutput) << "chunks =" << chunks; + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_buffer_time_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_period_time_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_periods_near: err = %1").arg(err); + } + } + if ( !fatal ) { + err = snd_pcm_hw_params(handle, hwparams); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params: err = %1").arg(err); + } + } + if( err < 0) { + qWarning()<<errMessage; + errorState = QAudio::OpenError; + emit errorChanged(errorState); + deviceState = QAudio::StoppedState; + return false; + } + snd_pcm_hw_params_get_buffer_size(hwparams,&buffer_frames); + buffer_size = snd_pcm_frames_to_bytes(handle,buffer_frames); + snd_pcm_hw_params_get_period_size(hwparams,&period_frames, &dir); + period_size = snd_pcm_frames_to_bytes(handle,period_frames); + snd_pcm_hw_params_get_buffer_time(hwparams,&buffer_time, &dir); + snd_pcm_hw_params_get_period_time(hwparams,&period_time, &dir); + + // Step 3: Set the desired SW parameters. + snd_pcm_sw_params_t *swparams; + snd_pcm_sw_params_alloca(&swparams); + snd_pcm_sw_params_current(handle, swparams); + snd_pcm_sw_params_set_start_threshold(handle,swparams,period_frames); + snd_pcm_sw_params_set_stop_threshold(handle,swparams,buffer_frames); + snd_pcm_sw_params_set_avail_min(handle, swparams,period_frames); + snd_pcm_sw_params(handle, swparams); + + // Step 4: Prepare audio + if(audioBuffer == 0) + audioBuffer = new char[snd_pcm_frames_to_bytes(handle,buffer_frames)]; + snd_pcm_prepare( handle ); + snd_pcm_start(handle); + + // Step 5: Setup timer + bytesAvailable = bytesFree(); + + // Step 6: Start audio processing + timer->start(period_time/1000); + + clockStamp.restart(); + timeStamp.restart(); + elapsedTimeOffset = 0; + errorState = QAudio::NoError; + totalTimeValue = 0; + opened = true; + + return true; +} + +void QAlsaAudioOutput::close() +{ + timer->stop(); + + if ( handle ) { + snd_pcm_drain( handle ); + snd_pcm_close( handle ); + handle = 0; + delete [] audioBuffer; + audioBuffer=0; + } + if(!pullMode && audioSource) { + delete audioSource; + audioSource = 0; + } + opened = false; +} + +int QAlsaAudioOutput::bytesFree() const +{ + if(resuming) + return period_size; + + if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState) + return 0; + + int frames = snd_pcm_avail_update(handle); + if (frames == -EPIPE) { + // Try and handle buffer underrun + int err = snd_pcm_recover(handle, frames, 0); + if (err < 0) + return 0; + else + frames = snd_pcm_avail_update(handle); + } else if (frames < 0) { + return 0; + } + + if ((int)frames > (int)buffer_frames) + frames = buffer_frames; + + return snd_pcm_frames_to_bytes(handle, frames); +} + +qint64 QAlsaAudioOutput::write( const char *data, qint64 len ) +{ + // Write out some audio data + if ( !handle ) + return 0; +#ifdef DEBUG_AUDIO + qDebug()<<"frames to write out = "<< + snd_pcm_bytes_to_frames( handle, (int)len )<<" ("<<len<<") bytes"; +#endif + int frames, err; + int space = bytesFree(); + + if (!space) + return 0; + + if (len < space) + space = len; + + frames = snd_pcm_bytes_to_frames(handle, space); + + if (m_volume < 1.0f) { + QVarLengthArray<char, 4096> out(space); + QAudioHelperInternal::qMultiplySamples(m_volume, settings, data, out.data(), space); + err = snd_pcm_writei(handle, out.constData(), frames); + } else { + err = snd_pcm_writei(handle, data, frames); + } + + if(err > 0) { + totalTimeValue += err; + resuming = false; + errorState = QAudio::NoError; + if (deviceState != QAudio::ActiveState) { + deviceState = QAudio::ActiveState; + emit stateChanged(deviceState); + } + return snd_pcm_frames_to_bytes( handle, err ); + } else + err = xrun_recovery(err); + + if(err < 0) { + close(); + errorState = QAudio::FatalError; + emit errorChanged(errorState); + deviceState = QAudio::StoppedState; + emit stateChanged(deviceState); + } + return 0; +} + +int QAlsaAudioOutput::periodSize() const +{ + return period_size; +} + +void QAlsaAudioOutput::setBufferSize(int value) +{ + if(deviceState == QAudio::StoppedState) + buffer_size = value; +} + +int QAlsaAudioOutput::bufferSize() const +{ + return buffer_size; +} + +void QAlsaAudioOutput::setNotifyInterval(int ms) +{ + intervalTime = qMax(0, ms); +} + +int QAlsaAudioOutput::notifyInterval() const +{ + return intervalTime; +} + +qint64 QAlsaAudioOutput::processedUSecs() const +{ + return qint64(1000000) * totalTimeValue / settings.sampleRate(); +} + +void QAlsaAudioOutput::resume() +{ + if(deviceState == QAudio::SuspendedState) { + int err = 0; + + if(handle) { + err = snd_pcm_prepare( handle ); + if(err < 0) + xrun_recovery(err); + + err = snd_pcm_start(handle); + if(err < 0) + xrun_recovery(err); + + bytesAvailable = (int)snd_pcm_frames_to_bytes(handle, buffer_frames); + } + resuming = true; + + deviceState = pullMode ? QAudio::ActiveState : QAudio::IdleState; + + errorState = QAudio::NoError; + timer->start(period_time/1000); + emit stateChanged(deviceState); + } +} + +void QAlsaAudioOutput::setFormat(const QAudioFormat& fmt) +{ + if (deviceState == QAudio::StoppedState) + settings = fmt; +} + +QAudioFormat QAlsaAudioOutput::format() const +{ + return settings; +} + +void QAlsaAudioOutput::suspend() +{ + if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState || resuming) { + snd_pcm_drain(handle); + timer->stop(); + deviceState = QAudio::SuspendedState; + errorState = QAudio::NoError; + emit stateChanged(deviceState); + } +} + +void QAlsaAudioOutput::userFeed() +{ + if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState) + return; +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :userFeed() OUT"; +#endif + if(deviceState == QAudio::IdleState) + bytesAvailable = bytesFree(); + + deviceReady(); +} + +bool QAlsaAudioOutput::deviceReady() +{ + if(pullMode) { + int l = 0; + int chunks = bytesAvailable/period_size; + if(chunks==0) { + bytesAvailable = bytesFree(); + return false; + } +#ifdef DEBUG_AUDIO + qDebug()<<"deviceReady() avail="<<bytesAvailable<<" bytes, period size="<<period_size<<" bytes"; + qDebug()<<"deviceReady() no. of chunks that can fit ="<<chunks<<", chunks in bytes ="<<period_size*chunks; +#endif + int input = period_frames*chunks; + if(input > (int)buffer_frames) + input = buffer_frames; + l = audioSource->read(audioBuffer,snd_pcm_frames_to_bytes(handle, input)); + + // reading can take a while and stream may have been stopped + if (!handle) + return false; + + if(l > 0) { + // Got some data to output + if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState) + return true; + qint64 bytesWritten = write(audioBuffer,l); + if (bytesWritten != l) + audioSource->seek(audioSource->pos()-(l-bytesWritten)); + bytesAvailable = bytesFree(); + + } else if(l == 0) { + // Did not get any data to output + bytesAvailable = bytesFree(); + if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) { + // Underrun + if (deviceState != QAudio::IdleState) { + errorState = QAudio::UnderrunError; + emit errorChanged(errorState); + deviceState = QAudio::IdleState; + emit stateChanged(deviceState); + } + } + + } else if(l < 0) { + close(); + deviceState = QAudio::StoppedState; + errorState = QAudio::IOError; + emit errorChanged(errorState); + emit stateChanged(deviceState); + } + } else { + bytesAvailable = bytesFree(); + if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) { + // Underrun + if (deviceState != QAudio::IdleState) { + errorState = QAudio::UnderrunError; + emit errorChanged(errorState); + deviceState = QAudio::IdleState; + emit stateChanged(deviceState); + } + } + } + + if(deviceState != QAudio::ActiveState) + return true; + + if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime) { + emit notify(); + elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime; + timeStamp.restart(); + } + return true; +} + +qint64 QAlsaAudioOutput::elapsedUSecs() const +{ + if (deviceState == QAudio::StoppedState) + return 0; + + return clockStamp.elapsed() * qint64(1000); +} + +void QAlsaAudioOutput::reset() +{ + if(handle) + snd_pcm_reset(handle); + + stop(); +} + +AlsaOutputPrivate::AlsaOutputPrivate(QAlsaAudioOutput* audio) +{ + audioDevice = qobject_cast<QAlsaAudioOutput*>(audio); +} + +AlsaOutputPrivate::~AlsaOutputPrivate() {} + +qint64 AlsaOutputPrivate::readData( char* data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + + return 0; +} + +qint64 AlsaOutputPrivate::writeData(const char* data, qint64 len) +{ + int retry = 0; + qint64 written = 0; + if((audioDevice->deviceState == QAudio::ActiveState) + ||(audioDevice->deviceState == QAudio::IdleState)) { + while(written < len) { + int chunk = audioDevice->write(data+written,(len-written)); + if(chunk <= 0) + retry++; + written+=chunk; + if(retry > 10) + return written; + } + } + return written; + +} + +QT_END_NAMESPACE + +#include "moc_qalsaaudiooutput_p.cpp" diff --git a/src/multimedia/audio/alsa/qalsaaudiooutput_p.h b/src/multimedia/audio/alsa/qalsaaudiooutput_p.h new file mode 100644 index 000000000..72b9c2e4c --- /dev/null +++ b/src/multimedia/audio/alsa/qalsaaudiooutput_p.h @@ -0,0 +1,164 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QAUDIOOUTPUTALSA_H +#define QAUDIOOUTPUTALSA_H + +#include <alsa/asoundlib.h> + +#include <QtCore/qfile.h> +#include <QtCore/qdebug.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include <QtMultimedia/qaudio.h> +#include <QtMultimedia/qaudiodeviceinfo.h> +#include <QtMultimedia/qaudiosystem.h> + +QT_BEGIN_NAMESPACE + +class QAlsaAudioOutput : public QAbstractAudioOutput +{ + friend class AlsaOutputPrivate; + Q_OBJECT +public: + QAlsaAudioOutput(const QByteArray &device); + ~QAlsaAudioOutput(); + + qint64 write( const char *data, qint64 len ); + + void start(QIODevice* device) override; + QIODevice* start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + int bytesFree() const override; + int periodSize() const override; + void setBufferSize(int value) override; + int bufferSize() const override; + void setNotifyInterval(int milliSeconds) override; + int notifyInterval() const override; + qint64 processedUSecs() const override; + qint64 elapsedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat& fmt) override; + QAudioFormat format() const override; + void setVolume(qreal) override; + qreal volume() const override; + + + QIODevice* audioSource; + QAudioFormat settings; + QAudio::Error errorState; + QAudio::State deviceState; + +private slots: + void userFeed(); + bool deviceReady(); + +signals: + void processMore(); + +private: + bool opened; + bool pullMode; + bool resuming; + int buffer_size; + int period_size; + int intervalTime; + qint64 totalTimeValue; + unsigned int buffer_time; + unsigned int period_time; + snd_pcm_uframes_t buffer_frames; + snd_pcm_uframes_t period_frames; + int xrun_recovery(int err); + + int setFormat(); + bool open(); + void close(); + + QTimer* timer; + QByteArray m_device; + int bytesAvailable; + QElapsedTimer timeStamp; + QElapsedTimer clockStamp; + qint64 elapsedTimeOffset; + char* audioBuffer; + snd_pcm_t* handle; + snd_pcm_access_t access; + snd_pcm_format_t pcmformat; + snd_pcm_hw_params_t *hwparams; + qreal m_volume; +}; + +class AlsaOutputPrivate : public QIODevice +{ + friend class QAlsaAudioOutput; + Q_OBJECT +public: + AlsaOutputPrivate(QAlsaAudioOutput* audio); + ~AlsaOutputPrivate(); + + qint64 readData( char* data, qint64 len) override; + qint64 writeData(const char* data, qint64 len) override; + +private: + QAlsaAudioOutput *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/multimedia/audio/alsa/qalsainterface.cpp b/src/multimedia/audio/alsa/qalsainterface.cpp new file mode 100644 index 000000000..361a6dc6d --- /dev/null +++ b/src/multimedia/audio/alsa/qalsainterface.cpp @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qalsainterface_p.h" +#include "qalsaaudiodeviceinfo_p.h" +#include "qalsaaudioinput_p.h" +#include "qalsaaudiooutput_p.h" + +QT_BEGIN_NAMESPACE + +QByteArray QAlsaInterface::defaultDevice(QAudio::Mode mode) const +{ + return QAlsaAudioDeviceInfo::defaultDevice(mode); +} + +QList<QByteArray> QAlsaInterface::availableDevices(QAudio::Mode mode) const +{ + return QAlsaAudioDeviceInfo::availableDevices(mode); +} + +QAbstractAudioInput *QAlsaInterface::createInput(const QByteArray &device) +{ + return new QAlsaAudioInput(device); +} + +QAbstractAudioOutput *QAlsaInterface::createOutput(const QByteArray &device) +{ + return new QAlsaAudioOutput(device); +} + +QAbstractAudioDeviceInfo *QAlsaInterface::createDeviceInfo(const QByteArray &device, QAudio::Mode mode) +{ + return new QAlsaAudioDeviceInfo(device, mode); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/alsa/qalsainterface_p.h b/src/multimedia/audio/alsa/qalsainterface_p.h new file mode 100644 index 000000000..05ade0f68 --- /dev/null +++ b/src/multimedia/audio/alsa/qalsainterface_p.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QALSAPLUGIN_H +#define QALSAPLUGIN_H + +#include <private/qaudiosysteminterface_p.h> + +QT_BEGIN_NAMESPACE + +class QAlsaInterface : public QAudioSystemInterface +{ +public: + QByteArray defaultDevice(QAudio::Mode mode) const override; + QList<QByteArray> availableDevices(QAudio::Mode mode) const override; + QAbstractAudioInput *createInput(const QByteArray &device) override; + QAbstractAudioOutput *createOutput(const QByteArray &device) override; + QAbstractAudioDeviceInfo *createDeviceInfo(const QByteArray &device, QAudio::Mode mode) override; +}; + +QT_END_NAMESPACE + +#endif // QALSAPLUGIN_H diff --git a/src/multimedia/audio/audio.pri b/src/multimedia/audio/audio.pri index fcdfe3cd7..54da659b2 100644 --- a/src/multimedia/audio/audio.pri +++ b/src/multimedia/audio/audio.pri @@ -7,7 +7,6 @@ PUBLIC_HEADERS += \ audio/qaudioinput.h \ audio/qaudiooutput.h \ audio/qaudiodeviceinfo.h \ - audio/qaudiosystemplugin.h \ audio/qaudiosystem.h \ audio/qsoundeffect.h \ audio/qaudioprobe.h \ @@ -18,6 +17,7 @@ PRIVATE_HEADERS += \ audio/qaudiodevicefactory_p.h \ audio/qwavedecoder_p.h \ audio/qsamplecache_p.h \ + audio/qaudiosysteminterface_p.h \ audio/qaudiohelpers_p.h \ SOURCES += \ @@ -26,7 +26,7 @@ SOURCES += \ audio/qaudiodeviceinfo.cpp \ audio/qaudiooutput.cpp \ audio/qaudioinput.cpp \ - audio/qaudiosystemplugin.cpp \ + audio/qaudiosysteminterface.cpp \ audio/qaudiosystem.cpp \ audio/qaudiodevicefactory.cpp \ audio/qsoundeffect.cpp \ @@ -45,3 +45,12 @@ qtConfig(pulseaudio) { PRIVATE_HEADERS += audio/qsoundeffect_qaudio_p.h SOURCES += audio/qsoundeffect_qaudio_p.cpp } + +#android:SUBDIRS += opensles +#qnx:SUBDIRS += qnx-audio +#win32:SUBDIRS += windowsaudio +#darwin:!watchos:SUBDIRS += coreaudio + +qtConfig(pulseaudio): include(pulseaudio/pulseaudio.pri) +qtConfig(alsa): include(alsa/alsa.pri) + diff --git a/src/multimedia/audio/pulseaudio/pulseaudio.pri b/src/multimedia/audio/pulseaudio/pulseaudio.pri new file mode 100644 index 000000000..59ac66089 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/pulseaudio.pri @@ -0,0 +1,15 @@ +QMAKE_USE_PRIVATE += pulseaudio + +HEADERS += audio/pulseaudio/qaudiointerface_pulse_p.h \ + audio/pulseaudio/qaudiodeviceinfo_pulse_p.h \ + audio/pulseaudio/qaudiooutput_pulse_p.h \ + audio/pulseaudio/qaudioinput_pulse_p.h \ + audio/pulseaudio/qaudioengine_pulse_p.h \ + audio/pulseaudio/qpulsehelpers_p.h + +SOURCES += audio/pulseaudio/qaudiointerface_pulse.cpp \ + audio/pulseaudio/qaudiodeviceinfo_pulse.cpp \ + audio/pulseaudio/qaudiooutput_pulse.cpp \ + audio/pulseaudio/qaudioinput_pulse.cpp \ + audio/pulseaudio/qaudioengine_pulse.cpp \ + audio/pulseaudio/qpulsehelpers.cpp diff --git a/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse.cpp b/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse.cpp new file mode 100644 index 000000000..2855a3218 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse.cpp @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qaudiodeviceinfo_pulse_p.h" +#include "qaudioengine_pulse_p.h" +#include "qpulsehelpers_p.h" + +QT_BEGIN_NAMESPACE + +QPulseAudioDeviceInfo::QPulseAudioDeviceInfo(const QByteArray &device, QAudio::Mode mode) + : m_device(device) + , m_mode(mode) +{ +} + +bool QPulseAudioDeviceInfo::isFormatSupported(const QAudioFormat &format) const +{ + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(format); + if (!pa_sample_spec_valid(&spec)) + return false; + + return true; +} + +QAudioFormat QPulseAudioDeviceInfo::preferredFormat() const +{ + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + QAudioFormat format = pulseEngine->m_preferredFormats.value(m_device); + return format; +} + +QString QPulseAudioDeviceInfo::deviceName() const +{ + return m_device; +} + +QStringList QPulseAudioDeviceInfo::supportedCodecs() +{ + return QStringList() << "audio/pcm"; +} + +QList<int> QPulseAudioDeviceInfo::supportedSampleRates() +{ + return QList<int>() << 8000 << 11025 << 22050 << 44100 << 48000; +} + +QList<int> QPulseAudioDeviceInfo::supportedChannelCounts() +{ + return QList<int>() << 1 << 2 << 4 << 6 << 8; +} + +QList<int> QPulseAudioDeviceInfo::supportedSampleSizes() +{ + return QList<int>() << 8 << 16 << 24 << 32; +} + +QList<QAudioFormat::Endian> QPulseAudioDeviceInfo::supportedByteOrders() +{ + return QList<QAudioFormat::Endian>() << QAudioFormat::BigEndian << QAudioFormat::LittleEndian; +} + +QList<QAudioFormat::SampleType> QPulseAudioDeviceInfo::supportedSampleTypes() +{ + return QList<QAudioFormat::SampleType>() << QAudioFormat::SignedInt << QAudioFormat::UnSignedInt << QAudioFormat::Float; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse_p.h b/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse_p.h new file mode 100644 index 000000000..1cec772c0 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiodeviceinfo_pulse_p.h @@ -0,0 +1,90 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QAUDIODEVICEINFOPULSE_H +#define QAUDIODEVICEINFOPULSE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qbytearray.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qlist.h> + +#include "qaudio.h" +#include "qaudiodeviceinfo.h" +#include "qaudiosystem.h" + +QT_BEGIN_NAMESPACE + +class QPulseAudioDeviceInfo : public QAbstractAudioDeviceInfo +{ + Q_OBJECT + +public: + QPulseAudioDeviceInfo(const QByteArray &device, QAudio::Mode mode); + ~QPulseAudioDeviceInfo() {} + + QAudioFormat preferredFormat() const override; + bool isFormatSupported(const QAudioFormat &format) const override; + QString deviceName() const override; + QStringList supportedCodecs() override; + QList<int> supportedSampleRates() override; + QList<int> supportedChannelCounts() override; + QList<int> supportedSampleSizes() override; + QList<QAudioFormat::Endian> supportedByteOrders() override; + QList<QAudioFormat::SampleType> supportedSampleTypes() override; + +private: + QByteArray m_device; + QAudio::Mode m_mode; +}; + +QT_END_NAMESPACE + +#endif + diff --git a/src/multimedia/audio/pulseaudio/qaudioengine_pulse.cpp b/src/multimedia/audio/pulseaudio/qaudioengine_pulse.cpp new file mode 100644 index 000000000..299c20081 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudioengine_pulse.cpp @@ -0,0 +1,482 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/qdebug.h> + +#include <qaudiodeviceinfo.h> +#include "qaudioengine_pulse_p.h" +#include "qaudiodeviceinfo_pulse_p.h" +#include "qaudiooutput_pulse_p.h" +#include "qpulsehelpers_p.h" +#include <sys/types.h> +#include <unistd.h> + +QT_BEGIN_NAMESPACE + +static void serverInfoCallback(pa_context *context, const pa_server_info *info, void *userdata) +{ + if (!info) { + qWarning() << QString("Failed to get server information: %s").arg(pa_strerror(pa_context_errno(context))); + return; + } + +#ifdef DEBUG_PULSE + char ss[PA_SAMPLE_SPEC_SNPRINT_MAX], cm[PA_CHANNEL_MAP_SNPRINT_MAX]; + + pa_sample_spec_snprint(ss, sizeof(ss), &info->sample_spec); + pa_channel_map_snprint(cm, sizeof(cm), &info->channel_map); + + qDebug() << QString("User name: %1\n" + "Host Name: %2\n" + "Server Name: %3\n" + "Server Version: %4\n" + "Default Sample Specification: %5\n" + "Default Channel Map: %6\n" + "Default Sink: %7\n" + "Default Source: %8\n").arg( + info->user_name, + info->host_name, + info->server_name, + info->server_version, + ss, + cm, + info->default_sink_name, + info->default_source_name); +#endif + + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + pulseEngine->m_serverLock.lockForWrite(); + pulseEngine->m_defaultSink = info->default_sink_name; + pulseEngine->m_defaultSource = info->default_source_name; + pulseEngine->m_serverLock.unlock(); + + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void sinkInfoCallback(pa_context *context, const pa_sink_info *info, int isLast, void *userdata) +{ + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + + if (isLast < 0) { + qWarning() << QString("Failed to get sink information: %s").arg(pa_strerror(pa_context_errno(context))); + return; + } + + if (isLast) { + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + return; + } + + Q_ASSERT(info); + +#ifdef DEBUG_PULSE + QMap<pa_sink_state, QString> stateMap; + stateMap[PA_SINK_INVALID_STATE] = "n/a"; + stateMap[PA_SINK_RUNNING] = "RUNNING"; + stateMap[PA_SINK_IDLE] = "IDLE"; + stateMap[PA_SINK_SUSPENDED] = "SUSPENDED"; + + qDebug() << QString("Sink #%1\n" + "\tState: %2\n" + "\tName: %3\n" + "\tDescription: %4\n" + ).arg(QString::number(info->index), + stateMap.value(info->state), + info->name, + info->description); +#endif + + QAudioFormat format = QPulseAudioInternal::sampleSpecToAudioFormat(info->sample_spec); + + QWriteLocker locker(&pulseEngine->m_sinkLock); + pulseEngine->m_preferredFormats.insert(info->name, format); + pulseEngine->m_sinks.insert(info->index, info->name); +} + +static void sourceInfoCallback(pa_context *context, const pa_source_info *info, int isLast, void *userdata) +{ + Q_UNUSED(context); + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + + if (isLast) { + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + return; + } + + Q_ASSERT(info); + +#ifdef DEBUG_PULSE + QMap<pa_source_state, QString> stateMap; + stateMap[PA_SOURCE_INVALID_STATE] = "n/a"; + stateMap[PA_SOURCE_RUNNING] = "RUNNING"; + stateMap[PA_SOURCE_IDLE] = "IDLE"; + stateMap[PA_SOURCE_SUSPENDED] = "SUSPENDED"; + + qDebug() << QString("Source #%1\n" + "\tState: %2\n" + "\tName: %3\n" + "\tDescription: %4\n" + ).arg(QString::number(info->index), + stateMap.value(info->state), + info->name, + info->description); +#endif + + QAudioFormat format = QPulseAudioInternal::sampleSpecToAudioFormat(info->sample_spec); + + QWriteLocker locker(&pulseEngine->m_sourceLock); + pulseEngine->m_preferredFormats.insert(info->name, format); + pulseEngine->m_sources.insert(info->index, info->name); +} + +static void event_cb(pa_context* context, pa_subscription_event_type_t t, uint32_t index, void* userdata) +{ + QPulseAudioEngine *pulseEngine = static_cast<QPulseAudioEngine*>(userdata); + + int type = t & PA_SUBSCRIPTION_EVENT_TYPE_MASK; + int facility = t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; + + switch (type) { + case PA_SUBSCRIPTION_EVENT_NEW: + case PA_SUBSCRIPTION_EVENT_CHANGE: + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SERVER: { + pa_operation *op = pa_context_get_server_info(context, serverInfoCallback, userdata); + if (op) + pa_operation_unref(op); + else + qWarning("PulseAudioService: failed to get server info"); + break; + } + case PA_SUBSCRIPTION_EVENT_SINK: { + pa_operation *op = pa_context_get_sink_info_by_index(context, index, sinkInfoCallback, userdata); + if (op) + pa_operation_unref(op); + else + qWarning("PulseAudioService: failed to get sink info"); + break; + } + case PA_SUBSCRIPTION_EVENT_SOURCE: { + pa_operation *op = pa_context_get_source_info_by_index(context, index, sourceInfoCallback, userdata); + if (op) + pa_operation_unref(op); + else + qWarning("PulseAudioService: failed to get source info"); + break; + } + default: + break; + } + break; + case PA_SUBSCRIPTION_EVENT_REMOVE: + switch (facility) { + case PA_SUBSCRIPTION_EVENT_SINK: + pulseEngine->m_sinkLock.lockForWrite(); + pulseEngine->m_preferredFormats.remove(pulseEngine->m_sinks.value(index)); + pulseEngine->m_sinks.remove(index); + pulseEngine->m_sinkLock.unlock(); + break; + case PA_SUBSCRIPTION_EVENT_SOURCE: + pulseEngine->m_sourceLock.lockForWrite(); + pulseEngine->m_preferredFormats.remove(pulseEngine->m_sources.value(index)); + pulseEngine->m_sources.remove(index); + pulseEngine->m_sourceLock.unlock(); + break; + default: + break; + } + break; + default: + break; + } +} + +static void contextStateCallbackInit(pa_context *context, void *userdata) +{ + Q_UNUSED(context); +#ifdef DEBUG_PULSE + qDebug() << QPulseAudioInternal::stateToQString(pa_context_get_state(context)); +#endif + QPulseAudioEngine *pulseEngine = reinterpret_cast<QPulseAudioEngine*>(userdata); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void contextStateCallback(pa_context *c, void *userdata) +{ + QPulseAudioEngine *self = reinterpret_cast<QPulseAudioEngine*>(userdata); + pa_context_state_t state = pa_context_get_state(c); + +#ifdef DEBUG_PULSE + qDebug() << QPulseAudioInternal::stateToQString(state); +#endif + + if (state == PA_CONTEXT_FAILED) + QMetaObject::invokeMethod(self, "onContextFailed", Qt::QueuedConnection); +} + +Q_GLOBAL_STATIC(QPulseAudioEngine, pulseEngine); + +QPulseAudioEngine::QPulseAudioEngine(QObject *parent) + : QObject(parent) + , m_mainLoopApi(0) + , m_context(0) + , m_prepared(false) +{ + prepare(); +} + +QPulseAudioEngine::~QPulseAudioEngine() +{ + if (m_prepared) + release(); +} + +void QPulseAudioEngine::prepare() +{ + bool keepGoing = true; + bool ok = true; + + m_mainLoop = pa_threaded_mainloop_new(); + if (m_mainLoop == 0) { + qWarning("PulseAudioService: unable to create pulseaudio mainloop"); + return; + } + + if (pa_threaded_mainloop_start(m_mainLoop) != 0) { + qWarning("PulseAudioService: unable to start pulseaudio mainloop"); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = 0; + return; + } + + m_mainLoopApi = pa_threaded_mainloop_get_api(m_mainLoop); + + lock(); + + m_context = pa_context_new(m_mainLoopApi, QString(QLatin1String("QtPulseAudio:%1")).arg(::getpid()).toLatin1().constData()); + + if (m_context == 0) { + qWarning("PulseAudioService: Unable to create new pulseaudio context"); + pa_threaded_mainloop_unlock(m_mainLoop); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = 0; + onContextFailed(); + return; + } + + pa_context_set_state_callback(m_context, contextStateCallbackInit, this); + + if (pa_context_connect(m_context, 0, (pa_context_flags_t)0, 0) < 0) { + qWarning("PulseAudioService: pa_context_connect() failed"); + pa_context_unref(m_context); + pa_threaded_mainloop_unlock(m_mainLoop); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = 0; + m_context = 0; + return; + } + + pa_threaded_mainloop_wait(m_mainLoop); + + while (keepGoing) { + switch (pa_context_get_state(m_context)) { + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + break; + + case PA_CONTEXT_READY: +#ifdef DEBUG_PULSE + qDebug("Connection established."); +#endif + keepGoing = false; + break; + + case PA_CONTEXT_TERMINATED: + qCritical("PulseAudioService: Context terminated."); + keepGoing = false; + ok = false; + break; + + case PA_CONTEXT_FAILED: + default: + qCritical() << QString("PulseAudioService: Connection failure: %1").arg(pa_strerror(pa_context_errno(m_context))); + keepGoing = false; + ok = false; + } + + if (keepGoing) + pa_threaded_mainloop_wait(m_mainLoop); + } + + if (ok) { + pa_context_set_state_callback(m_context, contextStateCallback, this); + + pa_context_set_subscribe_callback(m_context, event_cb, this); + pa_operation *op = pa_context_subscribe(m_context, + pa_subscription_mask_t(PA_SUBSCRIPTION_MASK_SINK | + PA_SUBSCRIPTION_MASK_SOURCE | + PA_SUBSCRIPTION_MASK_SERVER), + NULL, NULL); + if (op) + pa_operation_unref(op); + else + qWarning("PulseAudioService: failed to subscribe to context notifications"); + } else { + pa_context_unref(m_context); + m_context = 0; + } + + unlock(); + + if (ok) { + updateDevices(); + m_prepared = true; + } else { + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = 0; + onContextFailed(); + } +} + +void QPulseAudioEngine::release() +{ + if (!m_prepared) + return; + + if (m_context) { + pa_context_disconnect(m_context); + pa_context_unref(m_context); + m_context = 0; + } + + if (m_mainLoop) { + pa_threaded_mainloop_stop(m_mainLoop); + pa_threaded_mainloop_free(m_mainLoop); + m_mainLoop = 0; + } + + m_prepared = false; +} + +void QPulseAudioEngine::updateDevices() +{ + lock(); + + // Get default input and output devices + pa_operation *operation = pa_context_get_server_info(m_context, serverInfoCallback, this); + if (operation) { + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + pa_operation_unref(operation); + } else { + qWarning("PulseAudioService: failed to get server info"); + } + + // Get output devices + operation = pa_context_get_sink_info_list(m_context, sinkInfoCallback, this); + if (operation) { + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + pa_operation_unref(operation); + } else { + qWarning("PulseAudioService: failed to get sink info"); + } + + // Get input devices + operation = pa_context_get_source_info_list(m_context, sourceInfoCallback, this); + if (operation) { + while (pa_operation_get_state(operation) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + pa_operation_unref(operation); + } else { + qWarning("PulseAudioService: failed to get source info"); + } + + unlock(); +} + +void QPulseAudioEngine::onContextFailed() +{ + // Give a chance to the connected slots to still use the Pulse main loop before releasing it. + emit contextFailed(); + + release(); + + // Try to reconnect later + QTimer::singleShot(3000, this, SLOT(prepare())); +} + +QPulseAudioEngine *QPulseAudioEngine::instance() +{ + return pulseEngine(); +} + +QList<QByteArray> QPulseAudioEngine::availableDevices(QAudio::Mode mode) const +{ + QList<QByteArray> devices; + QByteArray defaultDevice; + + m_serverLock.lockForRead(); + + if (mode == QAudio::AudioOutput) { + QReadLocker locker(&m_sinkLock); + devices = m_sinks.values(); + defaultDevice = m_defaultSink; + } else { + QReadLocker locker(&m_sourceLock); + devices = m_sources.values(); + defaultDevice = m_defaultSource; + } + + m_serverLock.unlock(); + + // Swap the default device to index 0 + devices.removeOne(defaultDevice); + devices.prepend(defaultDevice); + + return devices; +} + +QByteArray QPulseAudioEngine::defaultDevice(QAudio::Mode mode) const +{ + return (mode == QAudio::AudioOutput) ? m_defaultSink : m_defaultSource; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/pulseaudio/qaudioengine_pulse_p.h b/src/multimedia/audio/pulseaudio/qaudioengine_pulse_p.h new file mode 100644 index 000000000..b6b0be996 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudioengine_pulse_p.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QPULSEAUDIOENGINE_H +#define QPULSEAUDIOENGINE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qmap.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qreadwritelock.h> +#include <pulse/pulseaudio.h> +#include "qpulsehelpers_p.h" +#include <qaudioformat.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioEngine : public QObject +{ + Q_OBJECT + +public: + QPulseAudioEngine(QObject *parent = 0); + ~QPulseAudioEngine(); + + static QPulseAudioEngine *instance(); + pa_threaded_mainloop *mainloop() { return m_mainLoop; } + pa_context *context() { return m_context; } + + inline void lock() + { + if (m_mainLoop) + pa_threaded_mainloop_lock(m_mainLoop); + } + + inline void unlock() + { + if (m_mainLoop) + pa_threaded_mainloop_unlock(m_mainLoop); + } + + inline void wait(pa_operation *op) + { + while (m_mainLoop && pa_operation_get_state(op) == PA_OPERATION_RUNNING) + pa_threaded_mainloop_wait(m_mainLoop); + } + + QList<QByteArray> availableDevices(QAudio::Mode mode) const; + QByteArray defaultDevice(QAudio::Mode mode) const; + +Q_SIGNALS: + void contextFailed(); + +private Q_SLOTS: + void prepare(); + void onContextFailed(); + +private: + void updateDevices(); + void release(); + +public: + QMap<int, QByteArray> m_sinks; + QMap<int, QByteArray> m_sources; + QMap<QByteArray, QAudioFormat> m_preferredFormats; + + QByteArray m_defaultSink; + QByteArray m_defaultSource; + + mutable QReadWriteLock m_sinkLock; + mutable QReadWriteLock m_sourceLock; + mutable QReadWriteLock m_serverLock; + +private: + pa_mainloop_api *m_mainLoopApi; + pa_threaded_mainloop *m_mainLoop; + pa_context *m_context; + bool m_prepared; + }; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/audio/pulseaudio/qaudioinput_pulse.cpp b/src/multimedia/audio/pulseaudio/qaudioinput_pulse.cpp new file mode 100644 index 000000000..3a0b84fce --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudioinput_pulse.cpp @@ -0,0 +1,682 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdebug.h> +#include <QtCore/qmath.h> +#include <private/qaudiohelpers_p.h> + +#include "qaudioinput_pulse_p.h" +#include "qaudioengine_pulse_p.h" +#include "qaudiodeviceinfo_pulse_p.h" +#include "qpulsehelpers_p.h" +#include <sys/types.h> +#include <unistd.h> + +QT_BEGIN_NAMESPACE + +const int PeriodTimeMs = 50; + +static void inputStreamReadCallback(pa_stream *stream, size_t length, void *userdata) +{ + Q_UNUSED(userdata); + Q_UNUSED(length); + Q_UNUSED(stream); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void inputStreamStateCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(userdata); + pa_stream_state_t state = pa_stream_get_state(stream); +#ifdef DEBUG_PULSE + qDebug() << "Stream state: " << QPulseAudioInternal::stateToQString(state); +#endif + switch (state) { + case PA_STREAM_CREATING: + break; + case PA_STREAM_READY: { +#ifdef DEBUG_PULSE + QPulseAudioInput *audioInput = static_cast<QPulseAudioInput*>(userdata); + const pa_buffer_attr *buffer_attr = pa_stream_get_buffer_attr(stream); + qDebug() << "*** maxlength: " << buffer_attr->maxlength; + qDebug() << "*** prebuf: " << buffer_attr->prebuf; + qDebug() << "*** fragsize: " << buffer_attr->fragsize; + qDebug() << "*** minreq: " << buffer_attr->minreq; + qDebug() << "*** tlength: " << buffer_attr->tlength; + + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(audioInput->format()); + qDebug() << "*** bytes_to_usec: " << pa_bytes_to_usec(buffer_attr->fragsize, &spec); +#endif + } + break; + case PA_STREAM_TERMINATED: + break; + case PA_STREAM_FAILED: + default: + qWarning() << QString("Stream error: %1").arg(pa_strerror(pa_context_errno(pa_stream_get_context(stream)))); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + break; + } +} + +static void inputStreamUnderflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(userdata); + Q_UNUSED(stream); + qWarning() << "Got a buffer underflow!"; +} + +static void inputStreamOverflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + qWarning() << "Got a buffer overflow!"; +} + +static void inputStreamSuccessCallback(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + Q_UNUSED(success); + + //if (!success) + //TODO: Is cork success? i->operation_success = success; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +QPulseAudioInput::QPulseAudioInput(const QByteArray &device) + : m_totalTimeValue(0) + , m_audioSource(0) + , m_errorState(QAudio::NoError) + , m_deviceState(QAudio::StoppedState) + , m_volume(qreal(1.0f)) + , m_pullMode(true) + , m_opened(false) + , m_bytesAvailable(0) + , m_bufferSize(0) + , m_periodSize(0) + , m_intervalTime(1000) + , m_periodTime(PeriodTimeMs) + , m_stream(0) + , m_device(device) +{ + m_timer = new QTimer(this); + connect(m_timer, SIGNAL(timeout()), SLOT(userFeed())); +} + +QPulseAudioInput::~QPulseAudioInput() +{ + close(); + disconnect(m_timer, SIGNAL(timeout())); + QCoreApplication::processEvents(); + delete m_timer; +} + +void QPulseAudioInput::setError(QAudio::Error error) +{ + if (m_errorState == error) + return; + + m_errorState = error; + emit errorChanged(error); +} + +QAudio::Error QPulseAudioInput::error() const +{ + return m_errorState; +} + +void QPulseAudioInput::setState(QAudio::State state) +{ + if (m_deviceState == state) + return; + + m_deviceState = state; + emit stateChanged(state); +} + +QAudio::State QPulseAudioInput::state() const +{ + return m_deviceState; +} + +void QPulseAudioInput::setFormat(const QAudioFormat &format) +{ + if (m_deviceState == QAudio::StoppedState) + m_format = format; +} + +QAudioFormat QPulseAudioInput::format() const +{ + return m_format; +} + +void QPulseAudioInput::start(QIODevice *device) +{ + setState(QAudio::StoppedState); + setError(QAudio::NoError); + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = 0; + } + + close(); + + if (!open()) + return; + + m_pullMode = true; + m_audioSource = device; + + setState(QAudio::ActiveState); +} + +QIODevice *QPulseAudioInput::start() +{ + setState(QAudio::StoppedState); + setError(QAudio::NoError); + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = 0; + } + + close(); + + if (!open()) + return nullptr; + + m_pullMode = false; + m_audioSource = new PulseInputPrivate(this); + m_audioSource->open(QIODevice::ReadOnly | QIODevice::Unbuffered); + + setState(QAudio::IdleState); + + return m_audioSource; +} + +void QPulseAudioInput::stop() +{ + if (m_deviceState == QAudio::StoppedState) + return; + + close(); + + setError(QAudio::NoError); + setState(QAudio::StoppedState); +} + +bool QPulseAudioInput::open() +{ + if (m_opened) + return true; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + setError(QAudio::FatalError); + setState(QAudio::StoppedState); + return false; + } + + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + + if (!pa_sample_spec_valid(&spec)) { + setError(QAudio::OpenError); + setState(QAudio::StoppedState); + return false; + } + + m_spec = spec; + +#ifdef DEBUG_PULSE +// QTime now(QTime::currentTime()); +// qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; +#endif + + if (m_streamName.isNull()) + m_streamName = QString(QLatin1String("QtmPulseStream-%1-%2")).arg(::getpid()).arg(quintptr(this)).toUtf8(); + +#ifdef DEBUG_PULSE + qDebug() << "Format: " << QPulseAudioInternal::sampleFormatToQString(spec.format); + qDebug() << "Rate: " << spec.rate; + qDebug() << "Channels: " << spec.channels; + qDebug() << "Frame size: " << pa_frame_size(&spec); +#endif + + pulseEngine->lock(); + pa_channel_map channel_map; + + pa_channel_map_init_extend(&channel_map, spec.channels, PA_CHANNEL_MAP_DEFAULT); + + if (!pa_channel_map_compatible(&channel_map, &spec)) + qWarning() << "Channel map doesn't match sample specification!"; + + m_stream = pa_stream_new(pulseEngine->context(), m_streamName.constData(), &spec, &channel_map); + + pa_stream_set_state_callback(m_stream, inputStreamStateCallback, this); + pa_stream_set_read_callback(m_stream, inputStreamReadCallback, this); + + pa_stream_set_underflow_callback(m_stream, inputStreamUnderflowCallback, this); + pa_stream_set_overflow_callback(m_stream, inputStreamOverflowCallback, this); + + m_periodSize = pa_usec_to_bytes(PeriodTimeMs*1000, &spec); + + int flags = 0; + pa_buffer_attr buffer_attr; + buffer_attr.maxlength = (uint32_t) -1; + buffer_attr.prebuf = (uint32_t) -1; + buffer_attr.tlength = (uint32_t) -1; + buffer_attr.minreq = (uint32_t) -1; + flags |= PA_STREAM_ADJUST_LATENCY; + + if (m_bufferSize > 0) + buffer_attr.fragsize = (uint32_t) m_bufferSize; + else + buffer_attr.fragsize = (uint32_t) m_periodSize; + + if (pa_stream_connect_record(m_stream, m_device.data(), &buffer_attr, (pa_stream_flags_t)flags) < 0) { + qWarning() << "pa_stream_connect_record() failed!"; + pa_stream_unref(m_stream); + m_stream = 0; + pulseEngine->unlock(); + setError(QAudio::OpenError); + setState(QAudio::StoppedState); + return false; + } + + while (pa_stream_get_state(m_stream) != PA_STREAM_READY) + pa_threaded_mainloop_wait(pulseEngine->mainloop()); + + const pa_buffer_attr *actualBufferAttr = pa_stream_get_buffer_attr(m_stream); + m_periodSize = actualBufferAttr->fragsize; + m_periodTime = pa_bytes_to_usec(m_periodSize, &spec) / 1000; + if (actualBufferAttr->tlength != (uint32_t)-1) + m_bufferSize = actualBufferAttr->tlength; + + pulseEngine->unlock(); + + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioInput::onPulseContextFailed); + + m_opened = true; + m_timer->start(m_periodTime); + + m_clockStamp.restart(); + m_timeStamp.restart(); + m_elapsedTimeOffset = 0; + m_totalTimeValue = 0; + + return true; +} + +void QPulseAudioInput::close() +{ + if (!m_opened) + return; + + m_timer->stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (m_stream) { + pulseEngine->lock(); + + pa_stream_set_state_callback(m_stream, 0, 0); + pa_stream_set_read_callback(m_stream, 0, 0); + pa_stream_set_underflow_callback(m_stream, 0, 0); + pa_stream_set_overflow_callback(m_stream, 0, 0); + + pa_stream_disconnect(m_stream); + pa_stream_unref(m_stream); + m_stream = 0; + + pulseEngine->unlock(); + } + + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioInput::onPulseContextFailed); + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = 0; + } + m_opened = false; +} + +int QPulseAudioInput::checkBytesReady() +{ + if (m_deviceState != QAudio::ActiveState && m_deviceState != QAudio::IdleState) { + m_bytesAvailable = 0; + } else { + m_bytesAvailable = pa_stream_readable_size(m_stream); + } + + return m_bytesAvailable; +} + +int QPulseAudioInput::bytesReady() const +{ + return qMax(m_bytesAvailable, 0); +} + +qint64 QPulseAudioInput::read(char *data, qint64 len) +{ + m_bytesAvailable = checkBytesReady(); + + setError(QAudio::NoError); + setState(QAudio::ActiveState); + + int readBytes = 0; + + if (!m_pullMode && !m_tempBuffer.isEmpty()) { + readBytes = qMin(static_cast<int>(len), m_tempBuffer.size()); + memcpy(data, m_tempBuffer.constData(), readBytes); + m_totalTimeValue += readBytes; + + if (readBytes < m_tempBuffer.size()) { + m_tempBuffer.remove(0, readBytes); + return readBytes; + } + + m_tempBuffer.clear(); + } + + while (pa_stream_readable_size(m_stream) > 0) { + size_t readLength = 0; + +#ifdef DEBUG_PULSE + qDebug() << "QPulseAudioInput::read -- " << pa_stream_readable_size(m_stream) << " bytes available from pulse audio"; +#endif + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pulseEngine->lock(); + + const void *audioBuffer; + + // Second and third parameters (audioBuffer and length) to pa_stream_peek are output parameters, + // the audioBuffer pointer is set to point to the actual pulse audio data, + // and the length is set to the length of this data. + if (pa_stream_peek(m_stream, &audioBuffer, &readLength) < 0) { + qWarning() << QString("pa_stream_peek() failed: %1").arg(pa_strerror(pa_context_errno(pa_stream_get_context(m_stream)))); + pulseEngine->unlock(); + return 0; + } + + qint64 actualLength = 0; + if (m_pullMode) { + QByteArray adjusted(readLength, Qt::Uninitialized); + applyVolume(audioBuffer, adjusted.data(), readLength); + actualLength = m_audioSource->write(adjusted); + + if (actualLength < qint64(readLength)) { + pulseEngine->unlock(); + + setError(QAudio::UnderrunError); + setState(QAudio::IdleState); + + return actualLength; + } + } else { + actualLength = qMin(static_cast<int>(len - readBytes), static_cast<int>(readLength)); + applyVolume(audioBuffer, data + readBytes, actualLength); + } + +#ifdef DEBUG_PULSE + qDebug() << "QPulseAudioInput::read -- wrote " << actualLength << " to client"; +#endif + + if (actualLength < qint64(readLength)) { +#ifdef DEBUG_PULSE + qDebug() << "QPulseAudioInput::read -- appending " << readLength - actualLength << " bytes of data to temp buffer"; +#endif + int diff = readLength - actualLength; + int oldSize = m_tempBuffer.size(); + m_tempBuffer.resize(m_tempBuffer.size() + diff); + applyVolume(static_cast<const char *>(audioBuffer) + actualLength, m_tempBuffer.data() + oldSize, diff); + QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); + } + + m_totalTimeValue += actualLength; + readBytes += actualLength; + + pa_stream_drop(m_stream); + pulseEngine->unlock(); + + if (!m_pullMode && readBytes >= len) + break; + + if (m_intervalTime && (m_timeStamp.elapsed() + m_elapsedTimeOffset) > m_intervalTime) { + emit notify(); + m_elapsedTimeOffset = m_timeStamp.elapsed() + m_elapsedTimeOffset - m_intervalTime; + m_timeStamp.restart(); + } + } + +#ifdef DEBUG_PULSE + qDebug() << "QPulseAudioInput::read -- returning after reading " << readBytes << " bytes"; +#endif + + return readBytes; +} + +void QPulseAudioInput::applyVolume(const void *src, void *dest, int len) +{ + if (m_volume < 1.f) + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, src, dest, len); + else + memcpy(dest, src, len); +} + +void QPulseAudioInput::resume() +{ + if (m_deviceState == QAudio::SuspendedState || m_deviceState == QAudio::IdleState) { + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_operation *operation; + + pulseEngine->lock(); + + operation = pa_stream_cork(m_stream, 0, inputStreamSuccessCallback, 0); + pulseEngine->wait(operation); + pa_operation_unref(operation); + + pulseEngine->unlock(); + + m_timer->start(m_periodTime); + + setState(QAudio::ActiveState); + setError(QAudio::NoError); + } +} + +void QPulseAudioInput::setVolume(qreal vol) +{ + if (qFuzzyCompare(m_volume, vol)) + return; + + m_volume = qBound(qreal(0), vol, qreal(1)); +} + +qreal QPulseAudioInput::volume() const +{ + return m_volume; +} + +void QPulseAudioInput::setBufferSize(int value) +{ + m_bufferSize = value; +} + +int QPulseAudioInput::bufferSize() const +{ + return m_bufferSize; +} + +int QPulseAudioInput::periodSize() const +{ + return m_periodSize; +} + +void QPulseAudioInput::setNotifyInterval(int ms) +{ + m_intervalTime = qMax(0, ms); +} + +int QPulseAudioInput::notifyInterval() const +{ + return m_intervalTime; +} + +qint64 QPulseAudioInput::processedUSecs() const +{ + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + qint64 result = pa_bytes_to_usec(m_totalTimeValue, &spec); + + return result; +} + +void QPulseAudioInput::suspend() +{ + if (m_deviceState == QAudio::ActiveState) { + setError(QAudio::NoError); + setState(QAudio::SuspendedState); + + m_timer->stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_operation *operation; + + pulseEngine->lock(); + + operation = pa_stream_cork(m_stream, 1, inputStreamSuccessCallback, 0); + pulseEngine->wait(operation); + pa_operation_unref(operation); + + pulseEngine->unlock(); + } +} + +void QPulseAudioInput::userFeed() +{ + if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState) + return; +#ifdef DEBUG_PULSE +// QTime now(QTime::currentTime()); +// qDebug()<< now.second() << "s " << now.msec() << "ms :userFeed() IN"; +#endif + deviceReady(); +} + +bool QPulseAudioInput::deviceReady() +{ + if (m_pullMode) { + // reads some audio data and writes it to QIODevice + read(0,0); + } else { + // emits readyRead() so user will call read() on QIODevice to get some audio data + if (m_audioSource != 0) { + PulseInputPrivate *a = qobject_cast<PulseInputPrivate*>(m_audioSource); + a->trigger(); + } + } + m_bytesAvailable = checkBytesReady(); + + if (m_deviceState != QAudio::ActiveState) + return true; + + if (m_intervalTime && (m_timeStamp.elapsed() + m_elapsedTimeOffset) > m_intervalTime) { + emit notify(); + m_elapsedTimeOffset = m_timeStamp.elapsed() + m_elapsedTimeOffset - m_intervalTime; + m_timeStamp.restart(); + } + + return true; +} + +qint64 QPulseAudioInput::elapsedUSecs() const +{ + if (m_deviceState == QAudio::StoppedState) + return 0; + + return m_clockStamp.elapsed() * qint64(1000); +} + +void QPulseAudioInput::reset() +{ + stop(); + m_bytesAvailable = 0; +} + +void QPulseAudioInput::onPulseContextFailed() +{ + close(); + + setError(QAudio::FatalError); + setState(QAudio::StoppedState); +} + +PulseInputPrivate::PulseInputPrivate(QPulseAudioInput *audio) +{ + m_audioDevice = qobject_cast<QPulseAudioInput*>(audio); +} + +qint64 PulseInputPrivate::readData(char *data, qint64 len) +{ + return m_audioDevice->read(data, len); +} + +qint64 PulseInputPrivate::writeData(const char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + return 0; +} + +void PulseInputPrivate::trigger() +{ + emit readyRead(); +} + +QT_END_NAMESPACE + +#include "moc_qaudioinput_pulse_p.cpp" diff --git a/src/multimedia/audio/pulseaudio/qaudioinput_pulse_p.h b/src/multimedia/audio/pulseaudio/qaudioinput_pulse_p.h new file mode 100644 index 000000000..dce212a25 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudioinput_pulse_p.h @@ -0,0 +1,161 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of other Qt classes. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#ifndef QAUDIOINPUTPULSE_H +#define QAUDIOINPUTPULSE_H + +#include <QtCore/qfile.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include "qaudio.h" +#include "qaudiodeviceinfo.h" +#include "qaudiosystem.h" + +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +class PulseInputPrivate; + +class QPulseAudioInput : public QAbstractAudioInput +{ + Q_OBJECT + +public: + QPulseAudioInput(const QByteArray &device); + ~QPulseAudioInput(); + + qint64 read(char *data, qint64 len); + + void start(QIODevice *device) override; + QIODevice *start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + int bytesReady() const override; + int periodSize() const override; + void setBufferSize(int value) override; + int bufferSize() const override; + void setNotifyInterval(int milliSeconds) override; + int notifyInterval() const override; + qint64 processedUSecs() const override; + qint64 elapsedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &format) override; + QAudioFormat format() const override; + + void setVolume(qreal volume) override; + qreal volume() const override; + + qint64 m_totalTimeValue; + QIODevice *m_audioSource; + QAudioFormat m_format; + QAudio::Error m_errorState; + QAudio::State m_deviceState; + qreal m_volume; + +private slots: + void userFeed(); + bool deviceReady(); + void onPulseContextFailed(); + +private: + void setState(QAudio::State state); + void setError(QAudio::Error error); + + void applyVolume(const void *src, void *dest, int len); + + int checkBytesReady(); + bool open(); + void close(); + + bool m_pullMode; + bool m_opened; + int m_bytesAvailable; + int m_bufferSize; + int m_periodSize; + int m_intervalTime; + unsigned int m_periodTime; + QTimer *m_timer; + qint64 m_elapsedTimeOffset; + pa_stream *m_stream; + QElapsedTimer m_timeStamp; + QElapsedTimer m_clockStamp; + QByteArray m_streamName; + QByteArray m_device; + QByteArray m_tempBuffer; + pa_sample_spec m_spec; +}; + +class PulseInputPrivate : public QIODevice +{ + Q_OBJECT +public: + PulseInputPrivate(QPulseAudioInput *audio); + ~PulseInputPrivate() {}; + + qint64 readData(char *data, qint64 len) override; + qint64 writeData(const char *data, qint64 len) override; + + void trigger(); + +private: + QPulseAudioInput *m_audioDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/audio/pulseaudio/qaudiointerface_pulse.cpp b/src/multimedia/audio/pulseaudio/qaudiointerface_pulse.cpp new file mode 100644 index 000000000..5c4a804b7 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiointerface_pulse.cpp @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <qaudiodeviceinfo.h> + +#include "qaudiointerface_pulse_p.h" +#include "qaudiodeviceinfo_pulse_p.h" +#include "qaudiooutput_pulse_p.h" +#include "qaudioinput_pulse_p.h" +#include "qaudioengine_pulse_p.h" + +QT_BEGIN_NAMESPACE + +QPulseAudioInterface::QPulseAudioInterface() + : QAudioSystemInterface() + , m_pulseEngine(QPulseAudioEngine::instance()) +{ +} + +QByteArray QPulseAudioInterface::defaultDevice(QAudio::Mode mode) const +{ + return m_pulseEngine->defaultDevice(mode); +} + +QList<QByteArray> QPulseAudioInterface::availableDevices(QAudio::Mode mode) const +{ + return m_pulseEngine->availableDevices(mode); +} + +QAbstractAudioInput *QPulseAudioInterface::createInput(const QByteArray &device) +{ + QPulseAudioInput *input = new QPulseAudioInput(device); + return input; +} + +QAbstractAudioOutput *QPulseAudioInterface::createOutput(const QByteArray &device) +{ + + QPulseAudioOutput *output = new QPulseAudioOutput(device); + return output; +} + +QAbstractAudioDeviceInfo *QPulseAudioInterface::createDeviceInfo(const QByteArray &device, QAudio::Mode mode) +{ + QPulseAudioDeviceInfo *deviceInfo = new QPulseAudioDeviceInfo(device, mode); + return deviceInfo; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/pulseaudio/qaudiointerface_pulse_p.h b/src/multimedia/audio/pulseaudio/qaudiointerface_pulse_p.h new file mode 100644 index 000000000..42f39d4c9 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiointerface_pulse_p.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QPULSEAUDIOPLUGIN_H +#define QPULSEAUDIOPLUGIN_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <private/qaudiosysteminterface_p.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioEngine; + +class QPulseAudioInterface : public QAudioSystemInterface +{ +public: + QPulseAudioInterface(); + + QByteArray defaultDevice(QAudio::Mode mode) const override; + QList<QByteArray> availableDevices(QAudio::Mode mode) const override; + QAbstractAudioInput *createInput(const QByteArray &device) override; + QAbstractAudioOutput *createOutput(const QByteArray &device) override; + QAbstractAudioDeviceInfo *createDeviceInfo(const QByteArray &device, QAudio::Mode mode) override; + +private: + QPulseAudioEngine *m_pulseEngine; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/audio/pulseaudio/qaudiooutput_pulse.cpp b/src/multimedia/audio/pulseaudio/qaudiooutput_pulse.cpp new file mode 100644 index 000000000..0dc666c88 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiooutput_pulse.cpp @@ -0,0 +1,739 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdebug.h> +#include <QtCore/qmath.h> +#include <private/qaudiohelpers_p.h> + +#include "qaudiooutput_pulse_p.h" +#include "qaudiodeviceinfo_pulse_p.h" +#include "qaudioengine_pulse_p.h" +#include "qpulsehelpers_p.h" +#include <sys/types.h> +#include <unistd.h> + +QT_BEGIN_NAMESPACE + +const int PeriodTimeMs = 20; +const int LowLatencyPeriodTimeMs = 10; +const int LowLatencyBufferSizeMs = 40; + +#define LOW_LATENCY_CATEGORY_NAME "game" + +static void outputStreamWriteCallback(pa_stream *stream, size_t length, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(length); + Q_UNUSED(userdata); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void outputStreamStateCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(userdata); + pa_stream_state_t state = pa_stream_get_state(stream); +#ifdef DEBUG_PULSE + qDebug() << "Stream state: " << QPulseAudioInternal::stateToQString(state); +#endif + switch (state) { + case PA_STREAM_CREATING: + case PA_STREAM_READY: + case PA_STREAM_TERMINATED: + break; + + case PA_STREAM_FAILED: + default: + qWarning() << QString("Stream error: %1").arg(pa_strerror(pa_context_errno(pa_stream_get_context(stream)))); + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); + break; + } +} + +static void outputStreamUnderflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + ((QPulseAudioOutput*)userdata)->streamUnderflowCallback(); +} + +static void outputStreamOverflowCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + qWarning() << "Got a buffer overflow!"; +} + +static void outputStreamLatencyCallback(pa_stream *stream, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(userdata); + +#ifdef DEBUG_PULSE + const pa_timing_info *info = pa_stream_get_timing_info(stream); + + qDebug() << "Write index corrupt: " << info->write_index_corrupt; + qDebug() << "Write index: " << info->write_index; + qDebug() << "Read index corrupt: " << info->read_index_corrupt; + qDebug() << "Read index: " << info->read_index; + qDebug() << "Sink usec: " << info->sink_usec; + qDebug() << "Configured sink usec: " << info->configured_sink_usec; +#endif +} + +static void outputStreamSuccessCallback(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(success); + Q_UNUSED(userdata); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_threaded_mainloop_signal(pulseEngine->mainloop(), 0); +} + +static void outputStreamDrainComplete(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(success); + Q_UNUSED(userdata); + +#ifdef DEBUG_PULSE + qDebug() << "Draining completed successfully: " << (bool)success; +#endif +} + +static void streamAdjustPrebufferCallback(pa_stream *stream, int success, void *userdata) +{ + Q_UNUSED(stream); + Q_UNUSED(success); + Q_UNUSED(userdata); + +#ifdef DEBUG_PULSE + qDebug() << "Adjust prebuffer completed successfully: " << (bool)success; +#endif +} + + +QPulseAudioOutput::QPulseAudioOutput(const QByteArray &device) + : m_device(device) + , m_errorState(QAudio::NoError) + , m_deviceState(QAudio::StoppedState) + , m_pullMode(true) + , m_opened(false) + , m_audioSource(0) + , m_periodTime(0) + , m_stream(0) + , m_notifyInterval(1000) + , m_periodSize(0) + , m_bufferSize(0) + , m_maxBufferSize(0) + , m_totalTimeValue(0) + , m_tickTimer(new QTimer(this)) + , m_audioBuffer(0) + , m_resuming(false) + , m_volume(1.0) +{ + connect(m_tickTimer, SIGNAL(timeout()), SLOT(userFeed())); +} + +QPulseAudioOutput::~QPulseAudioOutput() +{ + close(); + disconnect(m_tickTimer, SIGNAL(timeout())); + QCoreApplication::processEvents(); +} + +void QPulseAudioOutput::setError(QAudio::Error error) +{ + if (m_errorState == error) + return; + + m_errorState = error; + emit errorChanged(error); +} + +QAudio::Error QPulseAudioOutput::error() const +{ + return m_errorState; +} + +void QPulseAudioOutput::setState(QAudio::State state) +{ + if (m_deviceState == state) + return; + + m_deviceState = state; + emit stateChanged(state); +} + +QAudio::State QPulseAudioOutput::state() const +{ + return m_deviceState; +} + +void QPulseAudioOutput::streamUnderflowCallback() +{ + if (m_deviceState != QAudio::IdleState && !m_resuming) { + setError(QAudio::UnderrunError); + setState(QAudio::IdleState); + } +} + +void QPulseAudioOutput::start(QIODevice *device) +{ + setState(QAudio::StoppedState); + setError(QAudio::NoError); + + // Handle change of mode + if (m_audioSource && !m_pullMode) { + delete m_audioSource; + } + m_audioSource = 0; + + close(); + + m_pullMode = true; + m_audioSource = device; + + if (!open()) { + m_audioSource = 0; + return; + } + + setState(QAudio::ActiveState); +} + +QIODevice *QPulseAudioOutput::start() +{ + setState(QAudio::StoppedState); + setError(QAudio::NoError); + + // Handle change of mode + if (m_audioSource && !m_pullMode) { + delete m_audioSource; + } + m_audioSource = 0; + + close(); + + m_pullMode = false; + + if (!open()) + return nullptr; + + m_audioSource = new PulseOutputPrivate(this); + m_audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered); + + setState(QAudio::IdleState); + + return m_audioSource; +} + +bool QPulseAudioOutput::open() +{ + if (m_opened) + return true; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (!pulseEngine->context() || pa_context_get_state(pulseEngine->context()) != PA_CONTEXT_READY) { + setError(QAudio::FatalError); + setState(QAudio::StoppedState); + emit stateChanged(m_deviceState); + return false; + } + + pa_sample_spec spec = QPulseAudioInternal::audioFormatToSampleSpec(m_format); + + if (!pa_sample_spec_valid(&spec)) { + setError(QAudio::OpenError); + setState(QAudio::StoppedState); + emit stateChanged(m_deviceState); + return false; + } + + m_spec = spec; + m_totalTimeValue = 0; + + if (m_streamName.isNull()) + m_streamName = QString(QLatin1String("QtmPulseStream-%1-%2")).arg(::getpid()).arg(quintptr(this)).toUtf8(); + +#ifdef DEBUG_PULSE + qDebug() << "Format: " << QPulseAudioInternal::sampleFormatToQString(spec.format); + qDebug() << "Rate: " << spec.rate; + qDebug() << "Channels: " << spec.channels; + qDebug() << "Frame size: " << pa_frame_size(&spec); +#endif + + pulseEngine->lock(); + + qint64 bytesPerSecond = m_format.sampleRate() * m_format.channelCount() * m_format.sampleSize() / 8; + + pa_proplist *propList = pa_proplist_new(); + if (!m_category.isNull()) + pa_proplist_sets(propList, PA_PROP_MEDIA_ROLE, m_category.toLatin1().constData()); + + static const auto mapName = qEnvironmentVariable("QT_PA_CHANNEL_MAP"); + pa_channel_map_def_t mapDef = PA_CHANNEL_MAP_DEFAULT; + if (mapName == QLatin1String("ALSA")) + mapDef = PA_CHANNEL_MAP_ALSA; + else if (mapName == QLatin1String("AUX")) + mapDef = PA_CHANNEL_MAP_AUX; + else if (mapName == QLatin1String("WAVEEX")) + mapDef = PA_CHANNEL_MAP_WAVEEX; + else if (mapName == QLatin1String("OSS")) + mapDef = PA_CHANNEL_MAP_OSS; + else if (!mapName.isEmpty()) + qWarning() << "Unknown pulse audio channel mapping definition:" << mapName; + + pa_channel_map m; + auto channelMap = pa_channel_map_init_extend(&m, m_spec.channels, mapDef); + if (!channelMap) + qWarning() << "QAudioOutput: pa_channel_map_init_extend() Could not initialize channel map"; + + m_stream = pa_stream_new_with_proplist(pulseEngine->context(), m_streamName.constData(), &m_spec, channelMap, propList); + if (!m_stream) { + qWarning() << "QAudioOutput: pa_stream_new_with_proplist() failed!"; + pulseEngine->unlock(); + setError(QAudio::OpenError); + setState(QAudio::StoppedState); + emit stateChanged(m_deviceState); + return false; + } + + pa_proplist_free(propList); + + pa_stream_set_state_callback(m_stream, outputStreamStateCallback, this); + pa_stream_set_write_callback(m_stream, outputStreamWriteCallback, this); + + pa_stream_set_underflow_callback(m_stream, outputStreamUnderflowCallback, this); + pa_stream_set_overflow_callback(m_stream, outputStreamOverflowCallback, this); + pa_stream_set_latency_update_callback(m_stream, outputStreamLatencyCallback, this); + + if (m_bufferSize <= 0 && m_category == LOW_LATENCY_CATEGORY_NAME) { + m_bufferSize = bytesPerSecond * LowLatencyBufferSizeMs / qint64(1000); + } + + pa_buffer_attr requestedBuffer; + requestedBuffer.fragsize = (uint32_t)-1; + requestedBuffer.maxlength = (uint32_t)-1; + requestedBuffer.minreq = (uint32_t)-1; + requestedBuffer.prebuf = (uint32_t)-1; + requestedBuffer.tlength = m_bufferSize; + + if (pa_stream_connect_playback(m_stream, m_device.data(), (m_bufferSize > 0) ? &requestedBuffer : NULL, (pa_stream_flags_t)0, NULL, NULL) < 0) { + qWarning() << "pa_stream_connect_playback() failed!"; + pa_stream_unref(m_stream); + m_stream = 0; + pulseEngine->unlock(); + setError(QAudio::OpenError); + setState(QAudio::StoppedState); + emit stateChanged(m_deviceState); + return false; + } + + while (pa_stream_get_state(m_stream) != PA_STREAM_READY) + pa_threaded_mainloop_wait(pulseEngine->mainloop()); + + const pa_buffer_attr *buffer = pa_stream_get_buffer_attr(m_stream); + m_periodTime = (m_category == LOW_LATENCY_CATEGORY_NAME) ? LowLatencyPeriodTimeMs : PeriodTimeMs; + m_periodSize = pa_usec_to_bytes(m_periodTime*1000, &m_spec); + m_bufferSize = buffer->tlength; + m_maxBufferSize = buffer->maxlength; + m_audioBuffer = new char[m_maxBufferSize]; + + const qint64 streamSize = m_audioSource ? m_audioSource->size() : 0; + if (m_pullMode && streamSize > 0 && static_cast<qint64>(buffer->prebuf) > streamSize) { + pa_buffer_attr newBufferAttr; + newBufferAttr = *buffer; + newBufferAttr.prebuf = streamSize; + pa_operation *o = pa_stream_set_buffer_attr(m_stream, &newBufferAttr, streamAdjustPrebufferCallback, NULL); + if (o) + pa_operation_unref(o); + } + +#ifdef DEBUG_PULSE + qDebug() << "Buffering info:"; + qDebug() << "\tMax length: " << buffer->maxlength; + qDebug() << "\tTarget length: " << buffer->tlength; + qDebug() << "\tPre-buffering: " << buffer->prebuf; + qDebug() << "\tMinimum request: " << buffer->minreq; + qDebug() << "\tFragment size: " << buffer->fragsize; +#endif + + pulseEngine->unlock(); + + connect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioOutput::onPulseContextFailed); + + m_opened = true; + + m_tickTimer->start(m_periodTime); + + m_elapsedTimeOffset = 0; + m_timeStamp.restart(); + m_clockStamp.restart(); + + return true; +} + +void QPulseAudioOutput::close() +{ + if (!m_opened) + return; + + m_tickTimer->stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + if (m_stream) { + pulseEngine->lock(); + + pa_stream_set_state_callback(m_stream, 0, 0); + pa_stream_set_write_callback(m_stream, 0, 0); + pa_stream_set_underflow_callback(m_stream, 0, 0); + pa_stream_set_overflow_callback(m_stream, 0, 0); + pa_stream_set_latency_update_callback(m_stream, 0, 0); + + pa_operation *o = pa_stream_drain(m_stream, outputStreamDrainComplete, NULL); + if (o) + pa_operation_unref(o); + + pa_stream_disconnect(m_stream); + pa_stream_unref(m_stream); + m_stream = NULL; + + pulseEngine->unlock(); + } + + disconnect(pulseEngine, &QPulseAudioEngine::contextFailed, this, &QPulseAudioOutput::onPulseContextFailed); + + if (!m_pullMode && m_audioSource) { + delete m_audioSource; + m_audioSource = 0; + } + m_opened = false; + if (m_audioBuffer) { + delete[] m_audioBuffer; + m_audioBuffer = 0; + } +} + +void QPulseAudioOutput::userFeed() +{ + if (m_deviceState == QAudio::StoppedState || m_deviceState == QAudio::SuspendedState) + return; + + m_resuming = false; + + if (m_pullMode) { + int writableSize = bytesFree(); + int chunks = writableSize / m_periodSize; + if (chunks == 0) + return; + + int input = m_periodSize; // always request 1 chunk of data from user + if (input > m_maxBufferSize) + input = m_maxBufferSize; + + int audioBytesPulled = m_audioSource->read(m_audioBuffer, input); + Q_ASSERT(audioBytesPulled <= input); + if (m_audioBuffer && audioBytesPulled > 0) { + if (audioBytesPulled > input) { + qWarning() << "QPulseAudioOutput::userFeed() - Invalid audio data size provided from user:" + << audioBytesPulled << "should be less than" << input; + audioBytesPulled = input; + } + qint64 bytesWritten = write(m_audioBuffer, audioBytesPulled); + Q_ASSERT(bytesWritten == audioBytesPulled); //unfinished write should not happen since the data provided is less than writableSize + Q_UNUSED(bytesWritten); + + if (chunks > 1) { + // PulseAudio needs more data. Ask for it immediately. + QMetaObject::invokeMethod(this, "userFeed", Qt::QueuedConnection); + } + } + } + + if (m_deviceState != QAudio::ActiveState) + return; + + if (m_notifyInterval && (m_timeStamp.elapsed() + m_elapsedTimeOffset) > m_notifyInterval) { + emit notify(); + m_elapsedTimeOffset = m_timeStamp.restart() + m_elapsedTimeOffset - m_notifyInterval; + } +} + +qint64 QPulseAudioOutput::write(const char *data, qint64 len) +{ + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + pulseEngine->lock(); + + len = qMin(len, static_cast<qint64>(pa_stream_writable_size(m_stream))); + + if (m_volume < 1.0f) { + // Don't use PulseAudio volume, as it might affect all other streams of the same category + // or even affect the system volume if flat volumes are enabled + void *dest = NULL; + size_t nbytes = len; + if (pa_stream_begin_write(m_stream, &dest, &nbytes) < 0) { + qWarning("QAudioOutput(pulseaudio): pa_stream_begin_write, error = %s", + pa_strerror(pa_context_errno(pulseEngine->context()))); + setError(QAudio::IOError); + return 0; + } + + len = int(nbytes); + QAudioHelperInternal::qMultiplySamples(m_volume, m_format, data, dest, len); + data = reinterpret_cast<char *>(dest); + } + + if (pa_stream_write(m_stream, data, len, NULL, 0, PA_SEEK_RELATIVE) < 0) { + qWarning("QAudioOutput(pulseaudio): pa_stream_write, error = %s", + pa_strerror(pa_context_errno(pulseEngine->context()))); + setError(QAudio::IOError); + return 0; + } + + pulseEngine->unlock(); + m_totalTimeValue += len; + + setError(QAudio::NoError); + setState(QAudio::ActiveState); + + return len; +} + +void QPulseAudioOutput::stop() +{ + if (m_deviceState == QAudio::StoppedState) + return; + + close(); + + setError(QAudio::NoError); + setState(QAudio::StoppedState); +} + +int QPulseAudioOutput::bytesFree() const +{ + if (m_deviceState != QAudio::ActiveState && m_deviceState != QAudio::IdleState) + return 0; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pulseEngine->lock(); + int writableSize = pa_stream_writable_size(m_stream); + pulseEngine->unlock(); + return writableSize; +} + +int QPulseAudioOutput::periodSize() const +{ + return m_periodSize; +} + +void QPulseAudioOutput::setBufferSize(int value) +{ + m_bufferSize = value; +} + +int QPulseAudioOutput::bufferSize() const +{ + return m_bufferSize; +} + +void QPulseAudioOutput::setNotifyInterval(int ms) +{ + m_notifyInterval = qMax(0, ms); +} + +int QPulseAudioOutput::notifyInterval() const +{ + return m_notifyInterval; +} + +qint64 QPulseAudioOutput::processedUSecs() const +{ + qint64 result = qint64(1000000) * m_totalTimeValue / + (m_format.channelCount() * (m_format.sampleSize() / 8)) / + m_format.sampleRate(); + + return result; +} + +void QPulseAudioOutput::resume() +{ + if (m_deviceState == QAudio::SuspendedState) { + m_resuming = true; + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + + pulseEngine->lock(); + + pa_operation *operation = pa_stream_cork(m_stream, 0, outputStreamSuccessCallback, NULL); + pulseEngine->wait(operation); + pa_operation_unref(operation); + + operation = pa_stream_trigger(m_stream, outputStreamSuccessCallback, NULL); + pulseEngine->wait(operation); + pa_operation_unref(operation); + + pulseEngine->unlock(); + + m_tickTimer->start(m_periodTime); + + setState(m_pullMode ? QAudio::ActiveState : QAudio::IdleState); + setError(QAudio::NoError); + } +} + +void QPulseAudioOutput::setFormat(const QAudioFormat &format) +{ + m_format = format; +} + +QAudioFormat QPulseAudioOutput::format() const +{ + return m_format; +} + +void QPulseAudioOutput::suspend() +{ + if (m_deviceState == QAudio::ActiveState || m_deviceState == QAudio::IdleState) { + setError(QAudio::NoError); + setState(QAudio::SuspendedState); + + m_tickTimer->stop(); + + QPulseAudioEngine *pulseEngine = QPulseAudioEngine::instance(); + pa_operation *operation; + + pulseEngine->lock(); + + operation = pa_stream_cork(m_stream, 1, outputStreamSuccessCallback, NULL); + pulseEngine->wait(operation); + pa_operation_unref(operation); + + pulseEngine->unlock(); + } +} + +qint64 QPulseAudioOutput::elapsedUSecs() const +{ + if (m_deviceState == QAudio::StoppedState) + return 0; + + return m_clockStamp.elapsed() * qint64(1000); +} + +void QPulseAudioOutput::reset() +{ + stop(); +} + +PulseOutputPrivate::PulseOutputPrivate(QPulseAudioOutput *audio) +{ + m_audioDevice = qobject_cast<QPulseAudioOutput*>(audio); +} + +qint64 PulseOutputPrivate::readData(char *data, qint64 len) +{ + Q_UNUSED(data); + Q_UNUSED(len); + + return 0; +} + +qint64 PulseOutputPrivate::writeData(const char *data, qint64 len) +{ + int retry = 0; + qint64 written = 0; + + if ((m_audioDevice->m_deviceState == QAudio::ActiveState + || m_audioDevice->m_deviceState == QAudio::IdleState)) { + while(written < len) { + int chunk = m_audioDevice->write(data+written, (len-written)); + if (chunk <= 0) + retry++; + written+=chunk; + if (retry > 10) + return written; + } + } + + return written; +} + +void QPulseAudioOutput::setVolume(qreal vol) +{ + if (qFuzzyCompare(m_volume, vol)) + return; + + m_volume = qBound(qreal(0), vol, qreal(1)); +} + +qreal QPulseAudioOutput::volume() const +{ + return m_volume; +} + +void QPulseAudioOutput::setCategory(const QString &category) +{ + if (m_category != category) { + m_category = category; + } +} + +QString QPulseAudioOutput::category() const +{ + return m_category; +} + +void QPulseAudioOutput::onPulseContextFailed() +{ + close(); + + setError(QAudio::FatalError); + setState(QAudio::StoppedState); +} + +QT_END_NAMESPACE + +#include "moc_qaudiooutput_pulse_p.cpp" diff --git a/src/multimedia/audio/pulseaudio/qaudiooutput_pulse_p.h b/src/multimedia/audio/pulseaudio/qaudiooutput_pulse_p.h new file mode 100644 index 000000000..e11f2ab2f --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qaudiooutput_pulse_p.h @@ -0,0 +1,166 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QAUDIOOUTPUTPULSE_H +#define QAUDIOOUTPUTPULSE_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qfile.h> +#include <QtCore/qtimer.h> +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qiodevice.h> + +#include "qaudio.h" +#include "qaudiodeviceinfo.h" +#include "qaudiosystem.h" + +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +class QPulseAudioOutput : public QAbstractAudioOutput +{ + friend class PulseOutputPrivate; + Q_OBJECT + +public: + QPulseAudioOutput(const QByteArray &device); + ~QPulseAudioOutput(); + + void start(QIODevice *device) override; + QIODevice *start() override; + void stop() override; + void reset() override; + void suspend() override; + void resume() override; + int bytesFree() const override; + int periodSize() const override; + void setBufferSize(int value) override; + int bufferSize() const override; + void setNotifyInterval(int milliSeconds) override; + int notifyInterval() const override; + qint64 processedUSecs() const override; + qint64 elapsedUSecs() const override; + QAudio::Error error() const override; + QAudio::State state() const override; + void setFormat(const QAudioFormat &format) override; + QAudioFormat format() const override; + + void setVolume(qreal volume) override; + qreal volume() const override; + + void setCategory(const QString &category) override; + QString category() const override; + +public: + void streamUnderflowCallback(); + +private: + void setState(QAudio::State state); + void setError(QAudio::Error error); + + bool open(); + void close(); + qint64 write(const char *data, qint64 len); + +private Q_SLOTS: + void userFeed(); + void onPulseContextFailed(); + +private: + QByteArray m_device; + QByteArray m_streamName; + QAudioFormat m_format; + QAudio::Error m_errorState; + QAudio::State m_deviceState; + bool m_pullMode; + bool m_opened; + QIODevice *m_audioSource; + QTimer m_periodTimer; + int m_periodTime; + pa_stream *m_stream; + int m_notifyInterval; + int m_periodSize; + int m_bufferSize; + int m_maxBufferSize; + QElapsedTimer m_clockStamp; + qint64 m_totalTimeValue; + QTimer *m_tickTimer; + char *m_audioBuffer; + QElapsedTimer m_timeStamp; + qint64 m_elapsedTimeOffset; + bool m_resuming; + QString m_category; + + qreal m_volume; + pa_sample_spec m_spec; +}; + +class PulseOutputPrivate : public QIODevice +{ + friend class QPulseAudioOutput; + Q_OBJECT + +public: + PulseOutputPrivate(QPulseAudioOutput *audio); + virtual ~PulseOutputPrivate() {} + +protected: + qint64 readData(char *data, qint64 len) override; + qint64 writeData(const char *data, qint64 len) override; + +private: + QPulseAudioOutput *m_audioDevice; +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/audio/pulseaudio/qpulsehelpers.cpp b/src/multimedia/audio/pulseaudio/qpulsehelpers.cpp new file mode 100644 index 000000000..15bcc1074 --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qpulsehelpers.cpp @@ -0,0 +1,211 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qpulsehelpers_p.h" + +QT_BEGIN_NAMESPACE + +namespace QPulseAudioInternal +{ +pa_sample_spec audioFormatToSampleSpec(const QAudioFormat &format) +{ + pa_sample_spec spec; + + spec.rate = format.sampleRate(); + spec.channels = format.channelCount(); + spec.format = PA_SAMPLE_INVALID; + const bool isBigEndian = (format.byteOrder() == QAudioFormat::BigEndian); + + if (format.sampleType() == QAudioFormat::UnSignedInt) { + if (format.sampleSize() == 8) + spec.format = PA_SAMPLE_U8; + } else if (format.sampleType() == QAudioFormat::SignedInt) { + if (format.sampleSize() == 16) { + spec.format = isBigEndian ? PA_SAMPLE_S16BE : PA_SAMPLE_S16LE; + } else if (format.sampleSize() == 24) { + spec.format = isBigEndian ? PA_SAMPLE_S24BE : PA_SAMPLE_S24LE; + } else if (format.sampleSize() == 32) { + spec.format = isBigEndian ? PA_SAMPLE_S32BE : PA_SAMPLE_S32LE; + } + } else if (format.sampleType() == QAudioFormat::Float) { + if (format.sampleSize() == 32) + spec.format = isBigEndian ? PA_SAMPLE_FLOAT32BE : PA_SAMPLE_FLOAT32LE; + } + + return spec; +} + +#ifdef DEBUG_PULSE +QString stateToQString(pa_stream_state_t state) +{ + switch (state) + { + case PA_STREAM_UNCONNECTED: return "Unconnected"; + case PA_STREAM_CREATING: return "Creating"; + case PA_STREAM_READY: return "Ready"; + case PA_STREAM_FAILED: return "Failed"; + case PA_STREAM_TERMINATED: return "Terminated"; + } + + return QString("Unknown state: %0").arg(state); +} + +QString sampleFormatToQString(pa_sample_format format) +{ + switch (format) + { + case PA_SAMPLE_U8: return "Unsigned 8 Bit PCM."; + case PA_SAMPLE_ALAW: return "8 Bit a-Law "; + case PA_SAMPLE_ULAW: return "8 Bit mu-Law"; + case PA_SAMPLE_S16LE: return "Signed 16 Bit PCM, little endian (PC)."; + case PA_SAMPLE_S16BE: return "Signed 16 Bit PCM, big endian."; + case PA_SAMPLE_FLOAT32LE: return "32 Bit IEEE floating point, little endian (PC), range -1.0 to 1.0"; + case PA_SAMPLE_FLOAT32BE: return "32 Bit IEEE floating point, big endian, range -1.0 to 1.0"; + case PA_SAMPLE_S32LE: return "Signed 32 Bit PCM, little endian (PC)."; + case PA_SAMPLE_S32BE: return "Signed 32 Bit PCM, big endian."; + case PA_SAMPLE_S24LE: return "Signed 24 Bit PCM packed, little endian (PC)."; + case PA_SAMPLE_S24BE: return "Signed 24 Bit PCM packed, big endian."; + case PA_SAMPLE_S24_32LE: return "Signed 24 Bit PCM in LSB of 32 Bit words, little endian (PC)."; + case PA_SAMPLE_S24_32BE: return "Signed 24 Bit PCM in LSB of 32 Bit words, big endian."; + case PA_SAMPLE_MAX: return "Upper limit of valid sample types."; + case PA_SAMPLE_INVALID: return "Invalid sample format"; + } + + return QString("Invalid value: %0").arg(format); +} + +QString stateToQString(pa_context_state_t state) +{ + switch (state) + { + case PA_CONTEXT_UNCONNECTED: return "Unconnected"; + case PA_CONTEXT_CONNECTING: return "Connecting"; + case PA_CONTEXT_AUTHORIZING: return "Authorizing"; + case PA_CONTEXT_SETTING_NAME: return "Setting Name"; + case PA_CONTEXT_READY: return "Ready"; + case PA_CONTEXT_FAILED: return "Failed"; + case PA_CONTEXT_TERMINATED: return "Terminated"; + } + + return QString("Unknown state: %0").arg(state); +} +#endif + +QAudioFormat sampleSpecToAudioFormat(pa_sample_spec spec) +{ + QAudioFormat format; + format.setSampleRate(spec.rate); + format.setChannelCount(spec.channels); + format.setCodec("audio/pcm"); + + switch (spec.format) { + case PA_SAMPLE_U8: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::UnSignedInt); + format.setSampleSize(8); + break; + case PA_SAMPLE_ALAW: + // TODO: + break; + case PA_SAMPLE_ULAW: + // TODO: + break; + case PA_SAMPLE_S16LE: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(16); + break; + case PA_SAMPLE_S16BE: + format.setByteOrder(QAudioFormat::BigEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(16); + break; + case PA_SAMPLE_FLOAT32LE: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::Float); + format.setSampleSize(32); + break; + case PA_SAMPLE_FLOAT32BE: + format.setByteOrder(QAudioFormat::BigEndian); + format.setSampleType(QAudioFormat::Float); + format.setSampleSize(32); + break; + case PA_SAMPLE_S32LE: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(32); + break; + case PA_SAMPLE_S32BE: + format.setByteOrder(QAudioFormat::BigEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(32); + break; + case PA_SAMPLE_S24LE: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(24); + break; + case PA_SAMPLE_S24BE: + format.setByteOrder(QAudioFormat::BigEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(24); + break; + case PA_SAMPLE_S24_32LE: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(24); + break; + case PA_SAMPLE_S24_32BE: + format.setByteOrder(QAudioFormat::BigEndian); + format.setSampleType(QAudioFormat::SignedInt); + format.setSampleSize(24); + break; + case PA_SAMPLE_MAX: + case PA_SAMPLE_INVALID: + default: + format.setByteOrder(QAudioFormat::LittleEndian); + format.setSampleType(QAudioFormat::Unknown); + format.setSampleSize(0); + } + + return format; +} +} + +QT_END_NAMESPACE diff --git a/src/multimedia/audio/pulseaudio/qpulsehelpers_p.h b/src/multimedia/audio/pulseaudio/qpulsehelpers_p.h new file mode 100644 index 000000000..279cecc2f --- /dev/null +++ b/src/multimedia/audio/pulseaudio/qpulsehelpers_p.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QPULSEHELPER_H +#define QPULSEHELPER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qaudiodeviceinfo.h" +#include <qaudioformat.h> +#include <pulse/pulseaudio.h> + +QT_BEGIN_NAMESPACE + +namespace QPulseAudioInternal +{ +pa_sample_spec audioFormatToSampleSpec(const QAudioFormat &format); +QString stateToQString(pa_stream_state_t state); +QString stateToQString(pa_context_state_t state); +QString sampleFormatToQString(pa_sample_format format); +QAudioFormat sampleSpecToAudioFormat(pa_sample_spec spec); +} + +QT_END_NAMESPACE + +#endif diff --git a/src/multimedia/audio/qaudiodevicefactory.cpp b/src/multimedia/audio/qaudiodevicefactory.cpp index b81a48eee..c5cd2c098 100644 --- a/src/multimedia/audio/qaudiodevicefactory.cpp +++ b/src/multimedia/audio/qaudiodevicefactory.cpp @@ -40,23 +40,13 @@ #include <QtCore/qdebug.h> #include "qaudiosystem.h" -#include "qaudiosystemplugin.h" +#include "qaudiosysteminterface_p.h" #include "qmediapluginloader_p.h" #include "qaudiodevicefactory_p.h" QT_BEGIN_NAMESPACE -static QString defaultKey() -{ - return QStringLiteral("default"); -} - -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) -Q_GLOBAL_STATIC_WITH_ARGS(QMediaPluginLoader, audioLoader, - (QAudioSystemFactoryInterface_iid, QLatin1String("audio"), Qt::CaseInsensitive)) -#endif - class QNullDeviceInfo : public QAbstractAudioDeviceInfo { public: @@ -123,79 +113,41 @@ public: QList<QAudioDeviceInfo> QAudioDeviceFactory::availableDevices(QAudio::Mode mode) { QList<QAudioDeviceInfo> devices; -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) - QMediaPluginLoader* l = audioLoader(); - const auto keys = l->keys(); - for (const QString& key : keys) { - QAudioSystemFactoryInterface* plugin = qobject_cast<QAudioSystemFactoryInterface*>(l->instance(key)); - if (plugin) { - const auto availableDevices = plugin->availableDevices(mode); - for (const QByteArray& handle : availableDevices) - devices << QAudioDeviceInfo(key, handle, mode); - } + auto *iface = QAudioSystemInterface::instance(); + if (iface) { + const auto availableDevices = iface->availableDevices(mode); + for (const QByteArray& handle : availableDevices) + devices << QAudioDeviceInfo(handle, mode); } -#endif return devices; } QAudioDeviceInfo QAudioDeviceFactory::defaultDevice(QAudio::Mode mode) { -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) - QMediaPluginLoader* l = audioLoader(); - - // Check if there is a default plugin. - QAudioSystemFactoryInterface *plugin = qobject_cast<QAudioSystemFactoryInterface *>(l->instance(defaultKey())); - if (plugin) { + auto *iface = QAudioSystemInterface::instance(); + if (iface) { // Ask for the default device. - const QByteArray &device = plugin->defaultDevice(mode); + const QByteArray &device = iface->defaultDevice(mode); if (!device.isEmpty()) - return QAudioDeviceInfo(defaultKey(), device, mode); + return QAudioDeviceInfo(device, mode); // If there were no default devices then just pick the first device that's available. - const auto &devices = plugin->availableDevices(mode); + const auto &devices = iface->availableDevices(mode); if (!devices.isEmpty()) - return QAudioDeviceInfo(defaultKey(), devices.first(), mode); + return QAudioDeviceInfo(devices.first(), mode); } - // If no plugin is marked as default, check the other plugins. - // Note: We're going to prioritize plugins that report a default device. - const auto &keys = l->keys(); - QAudioDeviceInfo fallbackDevice; - for (const auto &key : keys) { - if (key == defaultKey()) - continue; - QAudioSystemFactoryInterface* plugin = qobject_cast<QAudioSystemFactoryInterface*>(l->instance(key)); - if (plugin) { - // Check if the plugin has the extent-ion interface. - const QByteArray &device = plugin->defaultDevice(mode); - if (!device.isEmpty()) - return QAudioDeviceInfo(key, device, mode); - if (fallbackDevice.isNull()) { - const auto &devices = plugin->availableDevices(mode); - if (!devices.isEmpty()) - fallbackDevice = QAudioDeviceInfo(key, devices.first(), mode); - } - } - } - - return fallbackDevice; -#else return QAudioDeviceInfo(); -#endif } -QAbstractAudioDeviceInfo* QAudioDeviceFactory::audioDeviceInfo(const QString &realm, const QByteArray &handle, QAudio::Mode mode) +QAbstractAudioDeviceInfo* QAudioDeviceFactory::audioDeviceInfo(const QByteArray &handle, QAudio::Mode mode) { QAbstractAudioDeviceInfo *rc = nullptr; -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) - QAudioSystemFactoryInterface* plugin = - qobject_cast<QAudioSystemFactoryInterface*>(audioLoader()->instance(realm)); - - if (plugin) - rc = plugin->createDeviceInfo(handle, mode); -#endif + auto *iface = QAudioSystemInterface::instance(); + if (iface) + rc = iface->createDeviceInfo(handle, mode); return rc == nullptr ? new QNullDeviceInfo() : rc; } @@ -215,16 +167,12 @@ QAbstractAudioInput* QAudioDeviceFactory::createInputDevice(QAudioDeviceInfo con if (deviceInfo.isNull()) return new QNullInputDevice(); -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) - QAudioSystemFactoryInterface* plugin = - qobject_cast<QAudioSystemFactoryInterface*>(audioLoader()->instance(deviceInfo.realm())); - - if (plugin) { - QAbstractAudioInput* p = plugin->createInput(deviceInfo.handle()); + auto *iface = QAudioSystemInterface::instance(); + if (iface) { + QAbstractAudioInput* p = iface->createInput(deviceInfo.handle()); if (p) p->setFormat(format); return p; } -#endif return new QNullInputDevice(); } @@ -234,16 +182,12 @@ QAbstractAudioOutput* QAudioDeviceFactory::createOutputDevice(QAudioDeviceInfo c if (deviceInfo.isNull()) return new QNullOutputDevice(); -#if !defined (QT_NO_LIBRARY) && !defined(QT_NO_SETTINGS) - QAudioSystemFactoryInterface* plugin = - qobject_cast<QAudioSystemFactoryInterface*>(audioLoader()->instance(deviceInfo.realm())); - - if (plugin) { - QAbstractAudioOutput* p = plugin->createOutput(deviceInfo.handle()); + auto *iface = QAudioSystemInterface::instance(); + if (iface) { + QAbstractAudioOutput* p = iface->createOutput(deviceInfo.handle()); if (p) p->setFormat(format); return p; } -#endif return new QNullOutputDevice(); } diff --git a/src/multimedia/audio/qaudiodevicefactory_p.h b/src/multimedia/audio/qaudiodevicefactory_p.h index 238be46a7..f5a7d0cde 100644 --- a/src/multimedia/audio/qaudiodevicefactory_p.h +++ b/src/multimedia/audio/qaudiodevicefactory_p.h @@ -73,7 +73,7 @@ public: static QAudioDeviceInfo defaultDevice(QAudio::Mode mode); - static QAbstractAudioDeviceInfo* audioDeviceInfo(const QString &realm, const QByteArray &handle, QAudio::Mode mode); + static QAbstractAudioDeviceInfo* audioDeviceInfo(const QByteArray &handle, QAudio::Mode mode); static QAbstractAudioInput* createDefaultInputDevice(QAudioFormat const &format); static QAbstractAudioOutput* createDefaultOutputDevice(QAudioFormat const &format); diff --git a/src/multimedia/audio/qaudiodeviceinfo.cpp b/src/multimedia/audio/qaudiodeviceinfo.cpp index 051ef8b3f..dcd2c24a9 100644 --- a/src/multimedia/audio/qaudiodeviceinfo.cpp +++ b/src/multimedia/audio/qaudiodeviceinfo.cpp @@ -61,30 +61,29 @@ public: { } - QAudioDeviceInfoPrivate(const QString &r, const QByteArray &h, QAudio::Mode m): - realm(r), handle(h), mode(m) + QAudioDeviceInfoPrivate(const QByteArray &h, QAudio::Mode m) + : handle(h), mode(m) { if (!handle.isEmpty()) - info = QAudioDeviceFactory::audioDeviceInfo(realm, handle, mode); + info = QAudioDeviceFactory::audioDeviceInfo(handle, mode); else info = nullptr; } QAudioDeviceInfoPrivate(const QAudioDeviceInfoPrivate &other): QSharedData(other), - realm(other.realm), handle(other.handle), mode(other.mode) + handle(other.handle), mode(other.mode) { - info = QAudioDeviceFactory::audioDeviceInfo(realm, handle, mode); + info = QAudioDeviceFactory::audioDeviceInfo(handle, mode); } QAudioDeviceInfoPrivate& operator=(const QAudioDeviceInfoPrivate &other) { delete info; - realm = other.realm; handle = other.handle; mode = other.mode; - info = QAudioDeviceFactory::audioDeviceInfo(realm, handle, mode); + info = QAudioDeviceFactory::audioDeviceInfo(handle, mode); return *this; } @@ -93,7 +92,6 @@ public: delete info; } - QString realm; QByteArray handle; QAudio::Mode mode; QAbstractAudioDeviceInfo* info; @@ -191,8 +189,7 @@ bool QAudioDeviceInfo::operator ==(const QAudioDeviceInfo &other) const { if (d == other.d) return true; - if (d->realm == other.d->realm - && d->mode == other.d->mode + if (d->mode == other.d->mode && d->handle == other.d->handle && deviceName() == other.deviceName()) return true; @@ -439,23 +436,12 @@ QList<QAudioDeviceInfo> QAudioDeviceInfo::availableDevices(QAudio::Mode mode) /*! \internal */ -QAudioDeviceInfo::QAudioDeviceInfo(const QString &realm, const QByteArray &handle, QAudio::Mode mode): - d(new QAudioDeviceInfoPrivate(realm, handle, mode)) +QAudioDeviceInfo::QAudioDeviceInfo(const QByteArray &handle, QAudio::Mode mode): + d(new QAudioDeviceInfoPrivate(handle, mode)) { } /*! - Returns the key that represents the audio plugin. - - \since 5.14 - \sa QAudioSystemPlugin -*/ -QString QAudioDeviceInfo::realm() const -{ - return d->realm; -} - -/*! \internal */ QByteArray QAudioDeviceInfo::handle() const diff --git a/src/multimedia/audio/qaudiodeviceinfo.h b/src/multimedia/audio/qaudiodeviceinfo.h index 015c8bad7..19556c430 100644 --- a/src/multimedia/audio/qaudiodeviceinfo.h +++ b/src/multimedia/audio/qaudiodeviceinfo.h @@ -87,7 +87,6 @@ public: QList<int> supportedSampleSizes() const; QList<QAudioFormat::Endian> supportedByteOrders() const; QList<QAudioFormat::SampleType> supportedSampleTypes() const; - QString realm() const; static QAudioDeviceInfo defaultInputDevice(); static QAudioDeviceInfo defaultOutputDevice(); @@ -95,7 +94,7 @@ public: static QList<QAudioDeviceInfo> availableDevices(QAudio::Mode mode); private: - QAudioDeviceInfo(const QString &realm, const QByteArray &handle, QAudio::Mode mode); + QAudioDeviceInfo(const QByteArray &handle, QAudio::Mode mode); QByteArray handle() const; QAudio::Mode mode() const; diff --git a/src/multimedia/audio/qaudiosystemplugin.cpp b/src/multimedia/audio/qaudiosysteminterface.cpp index 2df13b9ec..90861535d 100644 --- a/src/multimedia/audio/qaudiosystemplugin.cpp +++ b/src/multimedia/audio/qaudiosysteminterface.cpp @@ -37,14 +37,21 @@ ** ****************************************************************************/ +#include <private/qtmultimediaglobal_p.h> +#include "qaudiosysteminterface_p.h" -#include "qaudiosystemplugin.h" +#if QT_CONFIG(pulseaudio) +#include <private/qaudiointerface_pulse_p.h> +#elif QT_CONFIG(alsa) +#include <private/qalsainterface_p.h> +#endif QT_BEGIN_NAMESPACE /*! - \class QAudioSystemPlugin - \brief The QAudioSystemPlugin class provides an abstract base for audio plugins. + \class QAudioSystemInterface + \internal + \brief The QAudioSystemInterface class provides an abstract base for audio plugins. \ingroup multimedia \ingroup multimedia_audio @@ -80,50 +87,45 @@ QT_BEGIN_NAMESPACE QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).size() = 0 (dummy backend) */ -/*! - \fn QAudioSystemPlugin::QAudioSystemPlugin(QObject* parent) - - Constructs a new audio plugin with \a parent. - This is invoked automatically by the Q_PLUGIN_METADATA() macro. -*/ - -QAudioSystemPlugin::QAudioSystemPlugin(QObject* parent) : - QObject(parent) -{} +QAudioSystemInterface *QAudioSystemInterface::instance() +{ + static QAudioSystemInterface *system = nullptr; + if (!system) { +#if QT_CONFIG(pulseaudio) + system = new QPulseAudioInterface(); +#elif QT_CONFIG(alsa) + system = new QAlsaInterface(); +#endif + } + return system; +} -/*! - \fn QAudioSystemPlugin::~QAudioSystemPlugin() - - Destroys the audio plugin. - You never have to call this explicitly. Qt destroys a plugin automatically when it is no longer used. -*/ +QAudioSystemInterface::~QAudioSystemInterface() +{ -QAudioSystemPlugin::~QAudioSystemPlugin() -{} +} /*! - \fn QList<QByteArray> QAudioSystemPlugin::availableDevices(QAudio::Mode mode) const + \fn QList<QByteArray> QAudioSystemInterface::availableDevices(QAudio::Mode mode) const Returns a list of available audio devices for \a mode */ /*! - \fn QAbstractAudioInput* QAudioSystemPlugin::createInput(const QByteArray& device) + \fn QAbstractAudioInput* QAudioSystemInterface::createInput(const QByteArray& device) Returns a pointer to a QAbstractAudioInput created using \a device identifier */ /*! - \fn QAbstractAudioOutput* QAudioSystemPlugin::createOutput(const QByteArray& device) + \fn QAbstractAudioOutput* QAudioSystemInterface::createOutput(const QByteArray& device) Returns a pointer to a QAbstractAudioOutput created using \a device identifier */ /*! - \fn QAbstractAudioDeviceInfo* QAudioSystemPlugin::createDeviceInfo(const QByteArray& device, QAudio::Mode mode) + \fn QAbstractAudioDeviceInfo* QAudioSystemInterface::createDeviceInfo(const QByteArray& device, QAudio::Mode mode) Returns a pointer to a QAbstractAudioDeviceInfo created using \a device and \a mode */ QT_END_NAMESPACE - -#include "moc_qaudiosystemplugin.cpp" diff --git a/src/multimedia/audio/qaudiosystemplugin.h b/src/multimedia/audio/qaudiosysteminterface_p.h index 5c671f2b9..bc15cda81 100644 --- a/src/multimedia/audio/qaudiosystemplugin.h +++ b/src/multimedia/audio/qaudiosysteminterface_p.h @@ -41,6 +41,17 @@ #ifndef QAUDIOSYSTEMPLUGIN_H #define QAUDIOSYSTEMPLUGIN_H +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + #include <QtCore/qstring.h> #include <QtCore/qplugin.h> @@ -53,8 +64,11 @@ QT_BEGIN_NAMESPACE -struct Q_MULTIMEDIA_EXPORT QAudioSystemFactoryInterface +struct QAudioSystemInterface { + static QAudioSystemInterface *instance(); + + virtual ~QAudioSystemInterface(); virtual QList<QByteArray> availableDevices(QAudio::Mode) const = 0; virtual QAbstractAudioInput* createInput(const QByteArray& device) = 0; virtual QAbstractAudioOutput* createOutput(const QByteArray& device) = 0; @@ -62,20 +76,6 @@ struct Q_MULTIMEDIA_EXPORT QAudioSystemFactoryInterface virtual QByteArray defaultDevice(QAudio::Mode) const = 0; }; -#define QAudioSystemFactoryInterface_iid \ - "org.qt-project.qt.audiosystemfactory/5.0" -Q_DECLARE_INTERFACE(QAudioSystemFactoryInterface, QAudioSystemFactoryInterface_iid) - -class Q_MULTIMEDIA_EXPORT QAudioSystemPlugin : public QObject, public QAudioSystemFactoryInterface -{ - Q_OBJECT - Q_INTERFACES(QAudioSystemFactoryInterface) - -public: - explicit QAudioSystemPlugin(QObject *parent = nullptr); - ~QAudioSystemPlugin(); -}; - QT_END_NAMESPACE #endif // QAUDIOSYSTEMPLUGIN_H |