diff options
Diffstat (limited to 'src/multimedia/alsa')
-rw-r--r-- | src/multimedia/alsa/alsa.json | 3 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiodevice.cpp | 71 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiodevice_p.h | 49 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiosink.cpp | 698 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiosink_p.h | 121 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiosource.cpp | 770 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsaaudiosource_p.h | 142 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsamediadevices.cpp | 130 | ||||
-rw-r--r-- | src/multimedia/alsa/qalsamediadevices_p.h | 41 |
9 files changed, 2025 insertions, 0 deletions
diff --git a/src/multimedia/alsa/alsa.json b/src/multimedia/alsa/alsa.json new file mode 100644 index 000000000..71abb213c --- /dev/null +++ b/src/multimedia/alsa/alsa.json @@ -0,0 +1,3 @@ +{ + "Keys": [ "alsa" ] +} diff --git a/src/multimedia/alsa/qalsaaudiodevice.cpp b/src/multimedia/alsa/qalsaaudiodevice.cpp new file mode 100644 index 000000000..893375270 --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiodevice.cpp @@ -0,0 +1,71 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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 "qalsaaudiodevice_p.h" + +#include <alsa/version.h> + +QT_BEGIN_NAMESPACE + +QAlsaAudioDeviceInfo::QAlsaAudioDeviceInfo(const QByteArray &dev, const QString &desc, QAudioDevice::Mode mode) + : QAudioDevicePrivate(dev, mode) +{ + description = desc; + + checkSurround(); + + minimumChannelCount = 1; + maximumChannelCount = 2; + if (surround71) + maximumChannelCount = 8; + else if (surround40) + maximumChannelCount = 4; + else if (surround51) + maximumChannelCount = 6; + + minimumSampleRate = 8000; + maximumSampleRate = 48000; + + supportedSampleFormats = { + QAudioFormat::UInt8, + QAudioFormat::Int16, + QAudioFormat::Int32, + QAudioFormat::Float, + }; + + preferredFormat.setChannelCount(mode == QAudioDevice::Input ? 1 : 2); + preferredFormat.setSampleFormat(QAudioFormat::Float); + preferredFormat.setSampleRate(48000); +} + +QAlsaAudioDeviceInfo::~QAlsaAudioDeviceInfo() = default; + +void QAlsaAudioDeviceInfo::checkSurround() +{ + if (mode != QAudioDevice::Output) + return; + + surround40 = false; + surround51 = false; + surround71 = false; + + if (id.startsWith(QLatin1String("surround40"))) + surround40 = true; + if (id.startsWith(QLatin1String("surround51"))) + surround51 = true; + if (id.startsWith(QLatin1String("surround71"))) + surround71 = true; +} + +QT_END_NAMESPACE diff --git a/src/multimedia/alsa/qalsaaudiodevice_p.h b/src/multimedia/alsa/qalsaaudiodevice_p.h new file mode 100644 index 000000000..dcbc9e692 --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiodevice_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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 <private/qaudiodevice_p.h> +#include <private/qaudiosystem_p.h> + +QT_BEGIN_NAMESPACE + + +class QAlsaAudioDeviceInfo : public QAudioDevicePrivate +{ +public: + QAlsaAudioDeviceInfo(const QByteArray &dev, const QString &description, QAudioDevice::Mode mode); + ~QAlsaAudioDeviceInfo(); + +private: + void checkSurround(); + bool surround40{}; + bool surround51{}; + bool surround71{}; +}; + +QT_END_NAMESPACE + + +#endif // QALSAAUDIODEVICEINFO_H diff --git a/src/multimedia/alsa/qalsaaudiosink.cpp b/src/multimedia/alsa/qalsaaudiosink.cpp new file mode 100644 index 000000000..e515219a2 --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiosink.cpp @@ -0,0 +1,698 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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 "qalsaaudiosink_p.h" +#include "qalsaaudiodevice_p.h" +#include <QLoggingCategory> + +QT_BEGIN_NAMESPACE + +static Q_LOGGING_CATEGORY(lcAlsaOutput, "qt.multimedia.alsa.output") +//#define DEBUG_AUDIO 1 + +QAlsaAudioSink::QAlsaAudioSink(const QByteArray &device, QObject *parent) + : QPlatformAudioSink(parent) +{ + m_device = device; + + timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed); +} + +QAlsaAudioSink::~QAlsaAudioSink() +{ + close(); + disconnect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed); + QCoreApplication::processEvents(); + delete timer; +} + +void QAlsaAudioSink::setVolume(qreal vol) +{ + m_volume = vol; +} + +qreal QAlsaAudioSink::volume() const +{ + return m_volume; +} + +QAudio::Error QAlsaAudioSink::error() const +{ + return errorState; +} + +QAudio::State QAlsaAudioSink::state() const +{ + return deviceState; +} + +int QAlsaAudioSink::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 QAlsaAudioSink::setFormat() +{ + snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN; + + switch (settings.sampleFormat()) { + case QAudioFormat::UInt8: + pcmformat = SND_PCM_FORMAT_U8; + break; + case QAudioFormat::Int16: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_S16_BE; + else + pcmformat = SND_PCM_FORMAT_S16_LE; + break; + case QAudioFormat::Int32: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_S32_BE; + else + pcmformat = SND_PCM_FORMAT_S32_LE; + break; + case QAudioFormat::Float: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_FLOAT_BE; + else + pcmformat = SND_PCM_FORMAT_FLOAT_LE; + break; + default: + break; + } + + return pcmformat != SND_PCM_FORMAT_UNKNOWN + ? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat) + : -1; +} + +void QAlsaAudioSink::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; + + connect(audioSource, &QIODevice::readyRead, timer, [this] { + if (!timer->isActive()) { + timer->start(period_time / 1000); + } + }); + deviceState = QAudio::ActiveState; + + open(); + + emit stateChanged(deviceState); +} + +QIODevice* QAlsaAudioSink::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 QAlsaAudioSink::stop() +{ + if(deviceState == QAudio::StoppedState) + return; + errorState = QAudio::NoError; + deviceState = QAudio::StoppedState; + close(); + emit stateChanged(deviceState); +} + +bool QAlsaAudioSink::open() +{ + if(opened) + return true; + +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; +#endif + elapsedTimeOffset = 0; + + int dir; + int err = 0; + int count=0; + unsigned int sampleRate = settings.sampleRate(); + + if (!settings.isValid()) { + qWarning("QAudioSink: open error, invalid format."); + } else if (settings.sampleRate() <= 0) { + qWarning("QAudioSink: 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; + } + + // Step 1: try and open the device + while((count < 5) && (err < 0)) { + err=snd_pcm_open(&handle, m_device.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("QAudioSink: 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("QAudioSink: 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("QAudioSink: snd_pcm_hw_params_set_access: err = %1").arg(err); + } + } + if ( !fatal ) { + err = setFormat(); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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("QAudioSink: 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); + + elapsedTimeOffset = 0; + errorState = QAudio::NoError; + totalTimeValue = 0; + opened = true; + + return true; +} + +void QAlsaAudioSink::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; +} + +qsizetype QAlsaAudioSink::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 QAlsaAudioSink::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; +} + +void QAlsaAudioSink::setBufferSize(qsizetype value) +{ + if(deviceState == QAudio::StoppedState) + buffer_size = value; +} + +qsizetype QAlsaAudioSink::bufferSize() const +{ + return buffer_size; +} + +qint64 QAlsaAudioSink::processedUSecs() const +{ + return qint64(1000000) * totalTimeValue / settings.sampleRate(); +} + +void QAlsaAudioSink::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 = suspendedInState; + errorState = QAudio::NoError; + timer->start(period_time/1000); + emit stateChanged(deviceState); + } +} + +void QAlsaAudioSink::setFormat(const QAudioFormat& fmt) +{ + settings = fmt; +} + +QAudioFormat QAlsaAudioSink::format() const +{ + return settings; +} + +void QAlsaAudioSink::suspend() +{ + if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState || resuming) { + suspendedInState = deviceState; + snd_pcm_drain(handle); + timer->stop(); + deviceState = QAudio::SuspendedState; + errorState = QAudio::NoError; + emit stateChanged(deviceState); + } +} + +void QAlsaAudioSink::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 QAlsaAudioSink::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 + timer->stop(); + snd_pcm_drain(handle); + bytesAvailable = bytesFree(); + if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) { + // Underrun + if (deviceState != QAudio::IdleState) { + errorState = audioSource->atEnd() ? QAudio::NoError : 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; + + return true; +} + +void QAlsaAudioSink::reset() +{ + if(handle) + snd_pcm_reset(handle); + + stop(); +} + +AlsaOutputPrivate::AlsaOutputPrivate(QAlsaAudioSink* audio) +{ + audioDevice = qobject_cast<QAlsaAudioSink*>(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_qalsaaudiosink_p.cpp" diff --git a/src/multimedia/alsa/qalsaaudiosink_p.h b/src/multimedia/alsa/qalsaaudiosink_p.h new file mode 100644 index 000000000..0f5a5aa5a --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiosink_p.h @@ -0,0 +1,121 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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/qaudiodevice.h> +#include <private/qaudiosystem_p.h> + +QT_BEGIN_NAMESPACE + +class QAlsaAudioSink : public QPlatformAudioSink +{ + friend class AlsaOutputPrivate; + Q_OBJECT +public: + QAlsaAudioSink(const QByteArray &device, QObject *parent); + ~QAlsaAudioSink(); + + 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; + qsizetype bytesFree() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() 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 = nullptr; + QAudioFormat settings; + QAudio::Error errorState = QAudio::NoError; + QAudio::State deviceState = QAudio::StoppedState; + QAudio::State suspendedInState = QAudio::SuspendedState; + +private slots: + void userFeed(); + bool deviceReady(); + +signals: + void processMore(); + +private: + bool opened = false; + bool pullMode = true; + bool resuming = false; + int buffer_size = 0; + int period_size = 0; + qint64 totalTimeValue = 0; + unsigned int buffer_time = 100000; + unsigned int period_time = 20000; + 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 = nullptr; + QByteArray m_device; + int bytesAvailable = 0; + qint64 elapsedTimeOffset = 0; + char* audioBuffer = nullptr; + snd_pcm_t* handle = nullptr; + snd_pcm_access_t access = SND_PCM_ACCESS_RW_INTERLEAVED; + snd_pcm_hw_params_t *hwparams = nullptr; + qreal m_volume = 1.0f; +}; + +class AlsaOutputPrivate : public QIODevice +{ + friend class QAlsaAudioSink; + Q_OBJECT +public: + AlsaOutputPrivate(QAlsaAudioSink* audio); + ~AlsaOutputPrivate(); + + qint64 readData( char* data, qint64 len) override; + qint64 writeData(const char* data, qint64 len) override; + +private: + QAlsaAudioSink *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/multimedia/alsa/qalsaaudiosource.cpp b/src/multimedia/alsa/qalsaaudiosource.cpp new file mode 100644 index 000000000..ebf6e24e2 --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiosource.cpp @@ -0,0 +1,770 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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 "qalsaaudiosource_p.h" + +QT_BEGIN_NAMESPACE + +//#define DEBUG_AUDIO 1 + +QAlsaAudioSource::QAlsaAudioSource(const QByteArray &device, QObject *parent) + : QPlatformAudioSource(parent) +{ + 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; + 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, &QTimer::timeout, this, &QAlsaAudioSource::userFeed); +} + +QAlsaAudioSource::~QAlsaAudioSource() +{ + close(); + disconnect(timer, &QTimer::timeout, this, &QAlsaAudioSource::userFeed); + QCoreApplication::processEvents(); + delete timer; +} + +void QAlsaAudioSource::setVolume(qreal vol) +{ + m_volume = vol; +} + +qreal QAlsaAudioSource::volume() const +{ + return m_volume; +} + +QAudio::Error QAlsaAudioSource::error() const +{ + return errorState; +} + +QAudio::State QAlsaAudioSource::state() const +{ + return deviceState; +} + +void QAlsaAudioSource::setFormat(const QAudioFormat& fmt) +{ + if (deviceState == QAudio::StoppedState) + settings = fmt; +} + +QAudioFormat QAlsaAudioSource::format() const +{ + return settings; +} + +int QAlsaAudioSource::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 QAlsaAudioSource::setFormat() +{ + snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN; + + switch (settings.sampleFormat()) { + case QAudioFormat::UInt8: + pcmformat = SND_PCM_FORMAT_U8; + break; + case QAudioFormat::Int16: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_S16_BE; + else + pcmformat = SND_PCM_FORMAT_S16_LE; + break; + case QAudioFormat::Int32: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_S32_BE; + else + pcmformat = SND_PCM_FORMAT_S32_LE; + break; + case QAudioFormat::Float: + if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian) + pcmformat = SND_PCM_FORMAT_FLOAT_BE; + else + pcmformat = SND_PCM_FORMAT_FLOAT_LE; + break; + default: + break; + } + + return pcmformat != SND_PCM_FORMAT_UNKNOWN + ? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat) + : -1; +} + +void QAlsaAudioSource::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* QAlsaAudioSource::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 QAlsaAudioSource::stop() +{ + if(deviceState == QAudio::StoppedState) + return; + + deviceState = QAudio::StoppedState; + + close(); + emit stateChanged(deviceState); +} + +bool QAlsaAudioSource::open() +{ +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()"; +#endif + elapsedTimeOffset = 0; + + int dir; + int err = 0; + int count=0; + unsigned int sampleRate=settings.sampleRate(); + + if (!settings.isValid()) { + qWarning("QAudioSource: open error, invalid format."); + } else if (settings.sampleRate() <= 0) { + qWarning("QAudioSource: 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; + } + + + // Step 1: try and open the device + while((count < 5) && (err < 0)) { + err = snd_pcm_open(&handle, m_device.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("QAudioSource: 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("QAudioSource: 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("QAudioSource: snd_pcm_hw_params_set_access: err = %1").arg(err); + } + } + if ( !fatal ) { + err = setFormat(); + if ( err < 0 ) { + fatal = true; + errMessage = QString::fromLatin1("QAudioSource: 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("QAudioSource: 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("QAudioSource: 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("QAudioSource: 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("QAudioSource: 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("QAudioSource: 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("QAudioSource: 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, &QIODevice::readyRead, this, &QAlsaAudioSource::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 QAlsaAudioSource::close() +{ + timer->stop(); + + if ( handle ) { + snd_pcm_drop( handle ); + snd_pcm_close( handle ); + handle = 0; + } +} + +int QAlsaAudioSource::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; +} + +qsizetype QAlsaAudioSource::bytesReady() const +{ + return qMax(bytesAvailable, 0); +} + +qint64 QAlsaAudioSource::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 QAlsaAudioSource::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 QAlsaAudioSource::setBufferSize(qsizetype value) +{ + buffer_size = value; +} + +qsizetype QAlsaAudioSource::bufferSize() const +{ + return buffer_size; +} + +qint64 QAlsaAudioSource::processedUSecs() const +{ + qint64 result = qint64(1000000) * totalTimeValue / + settings.bytesPerFrame() / + settings.sampleRate(); + + return result; +} + +void QAlsaAudioSource::suspend() +{ + if(deviceState == QAudio::ActiveState||resuming) { + snd_pcm_drain(handle); + timer->stop(); + deviceState = QAudio::SuspendedState; + emit stateChanged(deviceState); + } +} + +void QAlsaAudioSource::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 QAlsaAudioSource::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; + } + } + + return true; +} + +void QAlsaAudioSource::reset() +{ + if(handle) + snd_pcm_reset(handle); + stop(); + bytesAvailable = 0; +} + +void QAlsaAudioSource::drain() +{ + if(handle) + snd_pcm_drain(handle); +} + +AlsaInputPrivate::AlsaInputPrivate(QAlsaAudioSource* audio) +{ + audioDevice = qobject_cast<QAlsaAudioSource*>(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_qalsaaudiosource_p.cpp" diff --git a/src/multimedia/alsa/qalsaaudiosource_p.h b/src/multimedia/alsa/qalsaaudiosource_p.h new file mode 100644 index 000000000..87487a6ad --- /dev/null +++ b/src/multimedia/alsa/qalsaaudiosource_p.h @@ -0,0 +1,142 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +// +// 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/qaudiodevice.h> +#include <private/qaudiosystem_p.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 QAlsaAudioSource : public QPlatformAudioSource +{ + Q_OBJECT +public: + QAlsaAudioSource(const QByteArray &device, QObject *parent); + ~QAlsaAudioSource(); + + 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; + qsizetype bytesReady() const override; + void setBufferSize(qsizetype value) override; + qsizetype bufferSize() const override; + qint64 processedUSecs() 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; + qint64 elapsedTimeOffset; + RingBuffer ringBuffer; + qsizetype bytesAvailable; + QByteArray m_device; + bool pullMode; + qsizetype 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(QAlsaAudioSource* audio); + ~AlsaInputPrivate(); + + qint64 readData( char* data, qint64 len) override; + qint64 writeData(const char* data, qint64 len) override; + + void trigger(); +private: + QAlsaAudioSource *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/multimedia/alsa/qalsamediadevices.cpp b/src/multimedia/alsa/qalsamediadevices.cpp new file mode 100644 index 000000000..9466fa0cd --- /dev/null +++ b/src/multimedia/alsa/qalsamediadevices.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qalsamediadevices_p.h" +#include "qmediadevices.h" +#include "qcameradevice_p.h" + +#include "private/qalsaaudiosource_p.h" +#include "private/qalsaaudiosink_p.h" +#include "private/qalsaaudiodevice_p.h" + +#include <alsa/asoundlib.h> + +QT_BEGIN_NAMESPACE + +namespace { + +struct free_char +{ + void operator()(char *c) const { ::free(c); } +}; + +using unique_str = std::unique_ptr<char, free_char>; + +bool operator==(const unique_str &str, std::string_view sv) +{ + return std::string_view{ str.get() } == sv; +} +bool operator!=(const unique_str &str, std::string_view sv) +{ + return !(str == sv); +} + +} // namespace + +QAlsaMediaDevices::QAlsaMediaDevices() + : QPlatformMediaDevices() +{ +} + +static QList<QAudioDevice> availableDevices(QAudioDevice::Mode mode) +{ + QList<QAudioDevice> devices; + + // Create a list of all current audio devices that support mode + void **hints; + if (snd_device_name_hint(-1, "pcm", &hints) < 0) { + qWarning() << "no alsa devices available"; + return devices; + } + + std::string_view filter = (mode == QAudioDevice::Input) ? "Input" : "Output"; + + QAlsaAudioDeviceInfo *sysdefault = nullptr; + + auto makeDeviceInfo = [&filter, mode](void *entry) -> QAlsaAudioDeviceInfo * { + unique_str name{ snd_device_name_get_hint(entry, "NAME") }; + if (name && name != "null") { + unique_str descr{ snd_device_name_get_hint(entry, "DESC") }; + unique_str io{ snd_device_name_get_hint(entry, "IOID") }; + + if (descr && (!io || (io == filter))) { + auto *infop = new QAlsaAudioDeviceInfo{ + name.get(), + QString::fromUtf8(descr.get()), + mode, + }; + return infop; + } + } + return nullptr; + }; + + bool hasDefault = false; + void **n = hints; + while (*n != NULL) { + QAlsaAudioDeviceInfo *infop = makeDeviceInfo(*n++); + + if (infop) { + devices.append(infop->create()); + if (!hasDefault && infop->id.startsWith("default")) { + infop->isDefault = true; + hasDefault = true; + } + if (!sysdefault && infop->id.startsWith("sysdefault")) + sysdefault = infop; + } + } + + if (!hasDefault && sysdefault) { + // Make "sysdefault" the default device if there is no "default" device exists + sysdefault->isDefault = true; + hasDefault = true; + } + if (!hasDefault && devices.size() > 0) { + // forcefully declare the first device as "default" + QAlsaAudioDeviceInfo *infop = makeDeviceInfo(hints[0]); + if (infop) { + infop->isDefault = true; + devices.prepend(infop->create()); + } + } + + snd_device_name_free_hint(hints); + return devices; +} + +QList<QAudioDevice> QAlsaMediaDevices::audioInputs() const +{ + return availableDevices(QAudioDevice::Input); +} + +QList<QAudioDevice> QAlsaMediaDevices::audioOutputs() const +{ + return availableDevices(QAudioDevice::Output); +} + +QPlatformAudioSource *QAlsaMediaDevices::createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QAlsaAudioSource(deviceInfo.id(), parent); +} + +QPlatformAudioSink *QAlsaMediaDevices::createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) +{ + return new QAlsaAudioSink(deviceInfo.id(), parent); +} + +QT_END_NAMESPACE diff --git a/src/multimedia/alsa/qalsamediadevices_p.h b/src/multimedia/alsa/qalsamediadevices_p.h new file mode 100644 index 000000000..d9fbb7c97 --- /dev/null +++ b/src/multimedia/alsa/qalsamediadevices_p.h @@ -0,0 +1,41 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QALSAMEDIADEVICES_H +#define QALSAMEDIADEVICES_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/qplatformmediadevices_p.h> +#include <qset.h> +#include <qaudio.h> + +QT_BEGIN_NAMESPACE + +class QAlsaEngine; + +class QAlsaMediaDevices : public QPlatformMediaDevices +{ +public: + QAlsaMediaDevices(); + + QList<QAudioDevice> audioInputs() const override; + QList<QAudioDevice> audioOutputs() const override; + QPlatformAudioSource *createAudioSource(const QAudioDevice &deviceInfo, + QObject *parent) override; + QPlatformAudioSink *createAudioSink(const QAudioDevice &deviceInfo, + QObject *parent) override; +}; + +QT_END_NAMESPACE + +#endif |