diff options
author | Christian Strømme <christian.stromme@digia.com> | 2013-11-23 00:14:15 +0100 |
---|---|---|
committer | The Qt Project <gerrit-noreply@qt-project.org> | 2014-01-30 18:15:36 +0100 |
commit | 2d54da2d39217e7b21ccafa9594513d554352a24 (patch) | |
tree | b9a620c05741fc81875c11fa351ea25713b1583c /src/plugins/alsa | |
parent | 0ab81ef59f35d103ec8174834c4fc2a4dcced453 (diff) |
Move win32 and Alsa audio backends into plugins.
Change-Id: I9835cf5ee97900569f26421a19543b485e933051
Reviewed-by: Yoann Lopes <yoann.lopes@digia.com>
Diffstat (limited to 'src/plugins/alsa')
-rw-r--r-- | src/plugins/alsa/alsa.json | 3 | ||||
-rw-r--r-- | src/plugins/alsa/alsa.pro | 23 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudiodeviceinfo.cpp | 464 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudiodeviceinfo.h | 122 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudioinput.cpp | 882 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudioinput.h | 188 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudiooutput.cpp | 860 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaaudiooutput.h | 170 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaplugin.cpp | 74 | ||||
-rw-r--r-- | src/plugins/alsa/qalsaplugin.h | 67 |
10 files changed, 2853 insertions, 0 deletions
diff --git a/src/plugins/alsa/alsa.json b/src/plugins/alsa/alsa.json new file mode 100644 index 000000000..c2b22dfec --- /dev/null +++ b/src/plugins/alsa/alsa.json @@ -0,0 +1,3 @@ +{ + "Keys": ["alsa"] +} diff --git a/src/plugins/alsa/alsa.pro b/src/plugins/alsa/alsa.pro new file mode 100644 index 000000000..481c57eaf --- /dev/null +++ b/src/plugins/alsa/alsa.pro @@ -0,0 +1,23 @@ +TARGET = qtaudio_alsa +QT += multimedia-private + +PLUGIN_TYPE = audio +PLUGIN_CLASS_NAME = QAlsaPlugin +load(qt_plugin) + +LIBS += -lasound + +HEADERS += \ + qalsaplugin.h \ + qalsaaudiodeviceinfo.h \ + qalsaaudioinput.h \ + qalsaaudiooutput.h + +SOURCES += \ + qalsaplugin.cpp \ + qalsaaudiodeviceinfo.cpp \ + qalsaaudioinput.cpp \ + qalsaaudiooutput.cpp + +OTHER_FILES += \ + alsa.json diff --git a/src/plugins/alsa/qalsaaudiodeviceinfo.cpp b/src/plugins/alsa/qalsaaudiodeviceinfo.cpp new file mode 100644 index 000000000..1e75c4661 --- /dev/null +++ b/src/plugins/alsa/qalsaaudiodeviceinfo.cpp @@ -0,0 +1,464 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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.h" + +#include <alsa/version.h> + +QT_BEGIN_NAMESPACE + +QAlsaAudioDeviceInfo::QAlsaAudioDeviceInfo(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; +} + +bool QAlsaAudioDeviceInfo::open() +{ + int err = 0; + QString dev = device; + QList<QByteArray> devices = availableDevices(mode); + + if(dev.compare(QLatin1String("default")) == 0) { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + if (devices.size() > 0) + dev = QLatin1String(devices.first().constData()); + else + return false; +#else + dev = QLatin1String("hw:0,0"); +#endif + } else { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + dev = device; +#else + int idx = 0; + char *name; + + QString shortName = device.mid(device.indexOf(QLatin1String("="),0)+1); + + while (snd_card_get_name(idx,&name) == 0) { + if(dev.contains(QLatin1String(name))) + break; + idx++; + } + dev = QString(QLatin1String("hw:%1,0")).arg(idx); +#endif + } + 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_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + dev = device; + if (dev.compare(QLatin1String("default")) == 0) { + QList<QByteArray> devices = availableDevices(QAudio::AudioOutput); + if (!devices.isEmpty()) + dev = QLatin1String(devices.first().constData()); + } +#else + if (dev.compare(QLatin1String("default")) == 0) { + dev = QLatin1String("hw:0,0"); + } else { + int idx = 0; + char *name; + + QString shortName = device.mid(device.indexOf(QLatin1String("="),0)+1); + + while(snd_card_get_name(idx,&name) == 0) { + if(shortName.compare(QLatin1String(name)) == 0) + break; + idx++; + } + dev = QString(QLatin1String("hw:%1,0")).arg(idx); + } +#endif + + 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; + QByteArray filter; + +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + // Create a list of all current audio devices that support mode + void **hints; + char *name, *descr, *io; + int card = -1; + + if(mode == QAudio::AudioInput) { + filter = "Input"; + } else { + filter = "Output"; + } + + while (snd_card_next(&card) == 0 && card >= 0) { + if (snd_device_name_hint(card, "pcm", &hints) < 0) + continue; + + void **n = hints; + 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))) { + QString deviceName = QLatin1String(name); + QString deviceDescription = QLatin1String(descr); + if (deviceDescription.contains(QLatin1String("Default Audio Device"))) + devices.prepend(deviceName.toLocal8Bit().constData()); + else + devices.append(deviceName.toLocal8Bit().constData()); + } + + 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); + idx++; + } +#endif + + if (devices.size() > 0) + devices.append("default"); + + return devices; +} + +QByteArray QAlsaAudioDeviceInfo::defaultInputDevice() +{ + QList<QByteArray> devices = availableDevices(QAudio::AudioInput); + if(devices.size() == 0) + return QByteArray(); + + return devices.first(); +} + +QByteArray QAlsaAudioDeviceInfo::defaultOutputDevice() +{ + QList<QByteArray> devices = availableDevices(QAudio::AudioOutput); + if(devices.size() == 0) + return QByteArray(); + + return devices.first(); +} + +void QAlsaAudioDeviceInfo::checkSurround() +{ + surround40 = false; + surround51 = false; + surround71 = false; + + void **hints; + char *name, *descr, *io; + int card = -1; + + while (snd_card_next(&card) == 0 && card >= 0) { + if (snd_device_name_hint(card, "pcm", &hints) < 0) + continue; + + void **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); + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/alsa/qalsaaudiodeviceinfo.h b/src/plugins/alsa/qalsaaudiodeviceinfo.h new file mode 100644 index 000000000..c1840ee9a --- /dev/null +++ b/src/plugins/alsa/qalsaaudiodeviceinfo.h @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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(QByteArray dev,QAudio::Mode mode); + ~QAlsaAudioDeviceInfo(); + + bool testSettings(const QAudioFormat& format) const; + void updateLists(); + QAudioFormat preferredFormat() const; + bool isFormatSupported(const QAudioFormat& format) const; + QString deviceName() const; + QStringList supportedCodecs(); + QList<int> supportedSampleRates(); + QList<int> supportedChannelCounts(); + QList<int> supportedSampleSizes(); + QList<QAudioFormat::Endian> supportedByteOrders(); + QList<QAudioFormat::SampleType> supportedSampleTypes(); + static QByteArray defaultInputDevice(); + static QByteArray defaultOutputDevice(); + static QList<QByteArray> availableDevices(QAudio::Mode); + +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/plugins/alsa/qalsaaudioinput.cpp b/src/plugins/alsa/qalsaaudioinput.cpp new file mode 100644 index 000000000..902dd57d7 --- /dev/null +++ b/src/plugins/alsa/qalsaaudioinput.cpp @@ -0,0 +1,882 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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 <QtMultimedia/private/qaudiohelpers_p.h> +#include "qalsaaudioinput.h" +#include "qalsaaudiodeviceinfo.h" + +QT_BEGIN_NAMESPACE + +//#define DEBUG_AUDIO 1 + +QAlsaAudioInput::QAlsaAudioInput(const QByteArray &device) +{ + bytesAvailable = 0; + handle = 0; + ahandler = 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; + + 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 InputPrivate(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; + } + + + QString dev = QString(QLatin1String(m_device.constData())); + QList<QByteArray> devices = QAlsaAudioDeviceInfo::availableDevices(QAudio::AudioInput); + if(dev.compare(QLatin1String("default")) == 0) { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + if (devices.size() > 0) + dev = QLatin1String(devices.first()); + else + return false; +#else + dev = QLatin1String("hw:0,0"); +#endif + } else { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + dev = QLatin1String(m_device); +#else + int idx = 0; + char *name; + + QString shortName = QLatin1String(m_device.mid(m_device.indexOf('=',0)+1).constData()); + + while(snd_card_get_name(idx,&name) == 0) { + if(qstrncmp(shortName.toLocal8Bit().constData(),name,shortName.length()) == 0) + break; + idx++; + } + dev = QString(QLatin1String("hw:%1,0")).arg(idx); +#endif + } + + // 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; + while(count < 5 && bytesToRead > 0) { + char buffer[bytesToRead]; + 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, frames); + bytesRead = snd_pcm_frames_to_bytes(handle, readFrames); + if (m_volume < 1.0f) + QAudioHelperInternal::qMultiplySamples(m_volume, settings, buffer, buffer, bytesRead); + + if (readFrames >= 0) { + ringBuffer.write(buffer, 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); + } else if(readFrames == -ESTRPIPE) { + err = snd_pcm_prepare(handle); + } + 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) { + 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 + InputPrivate* a = qobject_cast<InputPrivate*>(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()*1000; +} + +void QAlsaAudioInput::reset() +{ + if(handle) + snd_pcm_reset(handle); + stop(); + bytesAvailable = 0; +} + +void QAlsaAudioInput::drain() +{ + if(handle) + snd_pcm_drain(handle); +} + +InputPrivate::InputPrivate(QAlsaAudioInput* audio) +{ + audioDevice = qobject_cast<QAlsaAudioInput*>(audio); +} + +InputPrivate::~InputPrivate() +{ +} + +qint64 InputPrivate::readData( char* data, qint64 len) +{ + return audioDevice->read(data,len); +} + +qint64 InputPrivate::writeData(const char* data, qint64 len) +{ + Q_UNUSED(data) + Q_UNUSED(len) + return 0; +} + +void InputPrivate::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.cpp" diff --git a/src/plugins/alsa/qalsaaudioinput.h b/src/plugins/alsa/qalsaaudioinput.h new file mode 100644 index 000000000..6af566c8b --- /dev/null +++ b/src/plugins/alsa/qalsaaudioinput.h @@ -0,0 +1,188 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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/qdatetime.h> + +#include <QtMultimedia/qaudio.h> +#include <QtMultimedia/qaudiodeviceinfo.h> +#include <QtMultimedia/qaudiosystem.h> + +QT_BEGIN_NAMESPACE + + +class InputPrivate; + +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); + QIODevice* start(); + void stop(); + void reset(); + void suspend(); + void resume(); + int bytesReady() const; + int periodSize() const; + void setBufferSize(int value); + int bufferSize() const; + void setNotifyInterval(int milliSeconds); + int notifyInterval() const; + qint64 processedUSecs() const; + qint64 elapsedUSecs() const; + QAudio::Error error() const; + QAudio::State state() const; + void setFormat(const QAudioFormat& fmt); + QAudioFormat format() const; + void setVolume(qreal); + qreal volume() const; + 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; + QTime timeStamp; + QTime 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_async_handler_t* ahandler; + snd_pcm_access_t access; + snd_pcm_format_t pcmformat; + snd_timestamp_t* timestamp; + snd_pcm_hw_params_t *hwparams; + qreal m_volume; +}; + +class InputPrivate : public QIODevice +{ + Q_OBJECT +public: + InputPrivate(QAlsaAudioInput* audio); + ~InputPrivate(); + + qint64 readData( char* data, qint64 len); + qint64 writeData(const char* data, qint64 len); + + void trigger(); +private: + QAlsaAudioInput *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/plugins/alsa/qalsaaudiooutput.cpp b/src/plugins/alsa/qalsaaudiooutput.cpp new file mode 100644 index 000000000..192b63596 --- /dev/null +++ b/src/plugins/alsa/qalsaaudiooutput.cpp @@ -0,0 +1,860 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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 <QtMultimedia/private/qaudiohelpers_p.h> +#include "qalsaaudiooutput.h" +#include "qalsaaudiodeviceinfo.h" + +QT_BEGIN_NAMESPACE + +//#define DEBUG_AUDIO 1 + +QAlsaAudioOutput::QAlsaAudioOutput(const QByteArray &device) +{ + bytesAvailable = 0; + handle = 0; + ahandler = 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; +} + +void QAlsaAudioOutput::async_callback(snd_async_handler_t *ahandler) +{ + QAlsaAudioOutput* audioOut; + + audioOut = static_cast<QAlsaAudioOutput*> + (snd_async_handler_get_callback_private(ahandler)); + + if (audioOut && (audioOut->deviceState == QAudio::ActiveState || audioOut->resuming)) + audioOut->feedback(); +} + +int QAlsaAudioOutput::xrun_recovery(int err) +{ + int count = 0; + bool reset = false; + + 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 OutputPrivate(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 = QString(QLatin1String(m_device.constData())); + QList<QByteArray> devices = QAlsaAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + if(dev.compare(QLatin1String("default")) == 0) { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + if (devices.size() > 0) + dev = QLatin1String(devices.first()); + else + return false; +#else + dev = QLatin1String("hw:0,0"); +#endif + } else { +#if(SND_LIB_MAJOR == 1 && SND_LIB_MINOR == 0 && SND_LIB_SUBMINOR >= 14) + dev = QLatin1String(m_device); +#else + int idx = 0; + char *name; + + QString shortName = QLatin1String(m_device.mid(m_device.indexOf('=',0)+1).constData()); + + while (snd_card_get_name(idx,&name) == 0) { + if(qstrncmp(shortName.toLocal8Bit().constData(),name,shortName.length()) == 0) + break; + idx++; + } + dev = QString(QLatin1String("hw:%1,0")).arg(idx); +#endif + } + + // 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 { + if (maxBufferTime < buffer_time || buffer_time < minBufferTime || maxPeriodTime < period_time || minPeriodTime > period_time) { +#ifdef DEBUG_AUDIO + qDebug()<<"defaults out of range"; + qDebug()<<"pmin="<<minPeriodTime<<", pmax="<<maxPeriodTime<<", bmin="<<minBufferTime<<", bmax="<<maxBufferTime; +#endif + period_time = minPeriodTime; + if (period_time*4 <= maxBufferTime) { + // Use 4 periods if possible + buffer_time = period_time*4; + chunks = 4; + } else if (period_time*2 <= maxBufferTime) { + // Use 2 periods if possible + buffer_time = period_time*2; + chunks = 2; + } else { + qWarning()<<"QAudioOutput: alsa only supports single period!"; + fatal = true; + } +#ifdef DEBUG_AUDIO + qDebug()<<"used: buffer_time="<<buffer_time<<", period_time="<<period_time; +#endif + } + } + } + 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 callback and timer fallback + snd_async_add_pcm_handler(&ahandler, handle, async_callback, this); + 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) { + char out[space]; + QAudioHelperInternal::qMultiplySamples(m_volume, settings, data, out, space); + err = snd_pcm_writei(handle, out, 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 = QAudio::ActiveState; + + 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) { + 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(); +} + +void QAlsaAudioOutput::feedback() +{ + updateAvailable(); +} + + +void QAlsaAudioOutput::updateAvailable() +{ +#ifdef DEBUG_AUDIO + QTime now(QTime::currentTime()); + qDebug()<<now.second()<<"s "<<now.msec()<<"ms :updateAvailable()"; +#endif + bytesAvailable = bytesFree(); +} + +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) + 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()*1000; +} + +void QAlsaAudioOutput::reset() +{ + if(handle) + snd_pcm_reset(handle); + + stop(); +} + +OutputPrivate::OutputPrivate(QAlsaAudioOutput* audio) +{ + audioDevice = qobject_cast<QAlsaAudioOutput*>(audio); +} + +OutputPrivate::~OutputPrivate() {} + +qint64 OutputPrivate::readData( char* data, qint64 len) +{ + Q_UNUSED(data) + Q_UNUSED(len) + + return 0; +} + +qint64 OutputPrivate::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.cpp" diff --git a/src/plugins/alsa/qalsaaudiooutput.h b/src/plugins/alsa/qalsaaudiooutput.h new file mode 100644 index 000000000..67976a55b --- /dev/null +++ b/src/plugins/alsa/qalsaaudiooutput.h @@ -0,0 +1,170 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.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/qdatetime.h> + +#include <QtMultimedia/qaudio.h> +#include <QtMultimedia/qaudiodeviceinfo.h> +#include <QtMultimedia/qaudiosystem.h> + +QT_BEGIN_NAMESPACE + +class QAlsaAudioOutput : public QAbstractAudioOutput +{ + friend class OutputPrivate; + Q_OBJECT +public: + QAlsaAudioOutput(const QByteArray &device); + ~QAlsaAudioOutput(); + + qint64 write( const char *data, qint64 len ); + + void start(QIODevice* device); + QIODevice* start(); + void stop(); + void reset(); + void suspend(); + void resume(); + int bytesFree() const; + int periodSize() const; + void setBufferSize(int value); + int bufferSize() const; + void setNotifyInterval(int milliSeconds); + int notifyInterval() const; + qint64 processedUSecs() const; + qint64 elapsedUSecs() const; + QAudio::Error error() const; + QAudio::State state() const; + void setFormat(const QAudioFormat& fmt); + QAudioFormat format() const; + void setVolume(qreal); + qreal volume() const; + + + QIODevice* audioSource; + QAudioFormat settings; + QAudio::Error errorState; + QAudio::State deviceState; + +private slots: + void userFeed(); + void feedback(); + void updateAvailable(); + 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; + static void async_callback(snd_async_handler_t *ahandler); + int xrun_recovery(int err); + + int setFormat(); + bool open(); + void close(); + + QTimer* timer; + QByteArray m_device; + int bytesAvailable; + QTime timeStamp; + QTime clockStamp; + qint64 elapsedTimeOffset; + char* audioBuffer; + snd_pcm_t* handle; + snd_async_handler_t* ahandler; + snd_pcm_access_t access; + snd_pcm_format_t pcmformat; + snd_timestamp_t* timestamp; + snd_pcm_hw_params_t *hwparams; + qreal m_volume; +}; + +class OutputPrivate : public QIODevice +{ + friend class QAlsaAudioOutput; + Q_OBJECT +public: + OutputPrivate(QAlsaAudioOutput* audio); + ~OutputPrivate(); + + qint64 readData( char* data, qint64 len); + qint64 writeData(const char* data, qint64 len); + +private: + QAlsaAudioOutput *audioDevice; +}; + +QT_END_NAMESPACE + + +#endif diff --git a/src/plugins/alsa/qalsaplugin.cpp b/src/plugins/alsa/qalsaplugin.cpp new file mode 100644 index 000000000..6ed36580b --- /dev/null +++ b/src/plugins/alsa/qalsaplugin.cpp @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qalsaplugin.h" +#include "qalsaaudiodeviceinfo.h" +#include "qalsaaudioinput.h" +#include "qalsaaudiooutput.h" + +QT_BEGIN_NAMESPACE + +QAlsaPlugin::QAlsaPlugin(QObject *parent) + : QAudioSystemPlugin(parent) +{ +} + +QList<QByteArray> QAlsaPlugin::availableDevices(QAudio::Mode mode) const +{ + return QAlsaAudioDeviceInfo::availableDevices(mode); +} + +QAbstractAudioInput *QAlsaPlugin::createInput(const QByteArray &device) +{ + return new QAlsaAudioInput(device); +} + +QAbstractAudioOutput *QAlsaPlugin::createOutput(const QByteArray &device) +{ + return new QAlsaAudioOutput(device); +} + +QAbstractAudioDeviceInfo *QAlsaPlugin::createDeviceInfo(const QByteArray &device, QAudio::Mode mode) +{ + return new QAlsaAudioDeviceInfo(device, mode); +} + +QT_END_NAMESPACE diff --git a/src/plugins/alsa/qalsaplugin.h b/src/plugins/alsa/qalsaplugin.h new file mode 100644 index 000000000..6f524ac4a --- /dev/null +++ b/src/plugins/alsa/qalsaplugin.h @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QALSAPLUGIN_H +#define QALSAPLUGIN_H + +#include <QtMultimedia/qaudiosystemplugin.h> + +QT_BEGIN_NAMESPACE + +class QAlsaPlugin : public QAudioSystemPlugin +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "org.qt-project.qt.audiosystemfactory/5.0" FILE "alsa.json") + +public: + QAlsaPlugin(QObject *parent = 0); + ~QAlsaPlugin() {} + + QList<QByteArray> availableDevices(QAudio::Mode mode) const Q_DECL_OVERRIDE; + QAbstractAudioInput *createInput(const QByteArray &device) Q_DECL_OVERRIDE; + QAbstractAudioOutput *createOutput(const QByteArray &device) Q_DECL_OVERRIDE; + QAbstractAudioDeviceInfo *createDeviceInfo(const QByteArray &device, QAudio::Mode mode) Q_DECL_OVERRIDE; +}; + +QT_END_NAMESPACE + +#endif // QALSAPLUGIN_H |