blob: ee5aee9891b4f676f795691f95f520ba80a12c9f [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists for the convenience
// of other Qt classes. This header file may change from version to
// version without notice, or even be removed.
//
// INTERNAL USE ONLY: Do NOT use for any other purpose.
//
#include <QtCore/qcoreapplication.h>
#include <QtCore/qvarlengtharray.h>
#include <QtMultimedia/private/qaudiohelpers_p.h>
#include "qalsaaudiooutput.h"
#include "qalsaaudiodeviceinfo.h"
#include <QLoggingCategory>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcAlsaOutput, "qt.multimedia.alsa.output")
//#define DEBUG_AUDIO 1
QAlsaAudioOutput::QAlsaAudioOutput(const QByteArray &device)
{
bytesAvailable = 0;
handle = 0;
access = SND_PCM_ACCESS_RW_INTERLEAVED;
pcmformat = SND_PCM_FORMAT_S16;
buffer_frames = 0;
period_frames = 0;
buffer_size = 0;
period_size = 0;
buffer_time = 100000;
period_time = 20000;
totalTimeValue = 0;
intervalTime = 1000;
audioBuffer = 0;
errorState = QAudio::NoError;
deviceState = QAudio::StoppedState;
audioSource = 0;
pullMode = true;
resuming = false;
opened = false;
m_volume = 1.0f;
m_device = device;
timer = new QTimer(this);
connect(timer,SIGNAL(timeout()),SLOT(userFeed()));
}
QAlsaAudioOutput::~QAlsaAudioOutput()
{
close();
disconnect(timer, SIGNAL(timeout()));
QCoreApplication::processEvents();
delete timer;
}
void QAlsaAudioOutput::setVolume(qreal vol)
{
m_volume = vol;
}
qreal QAlsaAudioOutput::volume() const
{
return m_volume;
}
QAudio::Error QAlsaAudioOutput::error() const
{
return errorState;
}
QAudio::State QAlsaAudioOutput::state() const
{
return deviceState;
}
int QAlsaAudioOutput::xrun_recovery(int err)
{
int count = 0;
bool reset = false;
// ESTRPIPE is not available in all OSes where ALSA is available
int estrpipe = EIO;
#ifdef ESTRPIPE
estrpipe = ESTRPIPE;
#endif
if(err == -EPIPE) {
errorState = QAudio::UnderrunError;
emit errorChanged(errorState);
err = snd_pcm_prepare(handle);
if(err < 0)
reset = true;
} else if ((err == -estrpipe)||(err == -EIO)) {
errorState = QAudio::IOError;
emit errorChanged(errorState);
while((err = snd_pcm_resume(handle)) == -EAGAIN){
usleep(100);
count++;
if(count > 5) {
reset = true;
break;
}
}
if(err < 0) {
err = snd_pcm_prepare(handle);
if(err < 0)
reset = true;
}
}
if(reset) {
close();
open();
snd_pcm_prepare(handle);
return 0;
}
return err;
}
int QAlsaAudioOutput::setFormat()
{
snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN;
if(settings.sampleSize() == 8) {
pcmformat = SND_PCM_FORMAT_U8;
} else if(settings.sampleSize() == 16) {
if(settings.sampleType() == QAudioFormat::SignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_S16_LE;
else
pcmformat = SND_PCM_FORMAT_S16_BE;
} else if(settings.sampleType() == QAudioFormat::UnSignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_U16_LE;
else
pcmformat = SND_PCM_FORMAT_U16_BE;
}
} else if(settings.sampleSize() == 24) {
if(settings.sampleType() == QAudioFormat::SignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_S24_LE;
else
pcmformat = SND_PCM_FORMAT_S24_BE;
} else if(settings.sampleType() == QAudioFormat::UnSignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_U24_LE;
else
pcmformat = SND_PCM_FORMAT_U24_BE;
}
} else if(settings.sampleSize() == 32) {
if(settings.sampleType() == QAudioFormat::SignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_S32_LE;
else
pcmformat = SND_PCM_FORMAT_S32_BE;
} else if(settings.sampleType() == QAudioFormat::UnSignedInt) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_U32_LE;
else
pcmformat = SND_PCM_FORMAT_U32_BE;
} else if(settings.sampleType() == QAudioFormat::Float) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_FLOAT_LE;
else
pcmformat = SND_PCM_FORMAT_FLOAT_BE;
}
} else if(settings.sampleSize() == 64) {
if(settings.byteOrder() == QAudioFormat::LittleEndian)
pcmformat = SND_PCM_FORMAT_FLOAT64_LE;
else
pcmformat = SND_PCM_FORMAT_FLOAT64_BE;
}
return pcmformat != SND_PCM_FORMAT_UNKNOWN
? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat)
: -1;
}
void QAlsaAudioOutput::start(QIODevice* device)
{
if(deviceState != QAudio::StoppedState)
deviceState = QAudio::StoppedState;
errorState = QAudio::NoError;
// Handle change of mode
if(audioSource && !pullMode) {
delete audioSource;
audioSource = 0;
}
close();
pullMode = true;
audioSource = device;
deviceState = QAudio::ActiveState;
open();
emit stateChanged(deviceState);
}
QIODevice* QAlsaAudioOutput::start()
{
if(deviceState != QAudio::StoppedState)
deviceState = QAudio::StoppedState;
errorState = QAudio::NoError;
// Handle change of mode
if(audioSource && !pullMode) {
delete audioSource;
audioSource = 0;
}
close();
audioSource = new AlsaOutputPrivate(this);
audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
pullMode = false;
deviceState = QAudio::IdleState;
open();
emit stateChanged(deviceState);
return audioSource;
}
void QAlsaAudioOutput::stop()
{
if(deviceState == QAudio::StoppedState)
return;
errorState = QAudio::NoError;
deviceState = QAudio::StoppedState;
close();
emit stateChanged(deviceState);
}
bool QAlsaAudioOutput::open()
{
if(opened)
return true;
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()";
#endif
timeStamp.restart();
elapsedTimeOffset = 0;
int dir;
int err = 0;
int count=0;
unsigned int sampleRate=settings.sampleRate();
if (!settings.isValid()) {
qWarning("QAudioOutput: open error, invalid format.");
} else if (settings.sampleRate() <= 0) {
qWarning("QAudioOutput: open error, invalid sample rate (%d).",
settings.sampleRate());
} else {
err = -1;
}
if (err == 0) {
errorState = QAudio::OpenError;
deviceState = QAudio::StoppedState;
emit errorChanged(errorState);
return false;
}
QString dev;
#if SND_LIB_VERSION < 0x1000e // 1.0.14
if (m_device != "default")
dev = QAlsaAudioDeviceInfo::deviceFromCardName(m_device);
else
#endif
dev = m_device;
// Step 1: try and open the device
while((count < 5) && (err < 0)) {
err=snd_pcm_open(&handle,dev.toLocal8Bit().constData(),SND_PCM_STREAM_PLAYBACK,0);
if(err < 0)
count++;
}
if (( err < 0)||(handle == 0)) {
errorState = QAudio::OpenError;
emit errorChanged(errorState);
deviceState = QAudio::StoppedState;
return false;
}
snd_pcm_nonblock( handle, 0 );
// Step 2: Set the desired HW parameters.
snd_pcm_hw_params_alloca( &hwparams );
bool fatal = false;
QString errMessage;
unsigned int chunks = 8;
err = snd_pcm_hw_params_any( handle, hwparams );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_any: err = %1").arg(err);
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_rate_resample( handle, hwparams, 1 );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_rate_resample: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_access( handle, hwparams, access );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_access: err = %1").arg(err);
}
}
if ( !fatal ) {
err = setFormat();
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_format: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_channels( handle, hwparams, (unsigned int)settings.channelCount() );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_channels: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_rate_near( handle, hwparams, &sampleRate, 0 );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_rate_near: err = %1").arg(err);
}
}
if ( !fatal ) {
unsigned int maxBufferTime = 0;
unsigned int minBufferTime = 0;
unsigned int maxPeriodTime = 0;
unsigned int minPeriodTime = 0;
err = snd_pcm_hw_params_get_buffer_time_max(hwparams, &maxBufferTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_buffer_time_min(hwparams, &minBufferTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_period_time_max(hwparams, &maxPeriodTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_period_time_min(hwparams, &minPeriodTime, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: buffer/period min and max: err = %1").arg(err);
} else {
static unsigned user_buffer_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_BUFFER_TIME");
static unsigned user_period_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_PERIOD_TIME");
const bool outOfRange = maxBufferTime < buffer_time || buffer_time < minBufferTime || maxPeriodTime < period_time || minPeriodTime > period_time;
if (outOfRange || user_period_time || user_buffer_time) {
period_time = user_period_time ? user_period_time : minPeriodTime;
if (!user_buffer_time) {
chunks = maxBufferTime / period_time;
buffer_time = period_time * chunks;
} else {
buffer_time = user_buffer_time;
chunks = buffer_time / period_time;
}
}
qCDebug(lcAlsaOutput) << "buffer time: [" << minBufferTime << "-" << maxBufferTime << "] =" << buffer_time;
qCDebug(lcAlsaOutput) << "period time: [" << minPeriodTime << "-" << maxPeriodTime << "] =" << period_time;
qCDebug(lcAlsaOutput) << "chunks =" << chunks;
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_buffer_time_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_period_time_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params_set_periods_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params(handle, hwparams);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioOutput: snd_pcm_hw_params: err = %1").arg(err);
}
}
if( err < 0) {
qWarning()<<errMessage;
errorState = QAudio::OpenError;
emit errorChanged(errorState);
deviceState = QAudio::StoppedState;
return false;
}
snd_pcm_hw_params_get_buffer_size(hwparams,&buffer_frames);
buffer_size = snd_pcm_frames_to_bytes(handle,buffer_frames);
snd_pcm_hw_params_get_period_size(hwparams,&period_frames, &dir);
period_size = snd_pcm_frames_to_bytes(handle,period_frames);
snd_pcm_hw_params_get_buffer_time(hwparams,&buffer_time, &dir);
snd_pcm_hw_params_get_period_time(hwparams,&period_time, &dir);
// Step 3: Set the desired SW parameters.
snd_pcm_sw_params_t *swparams;
snd_pcm_sw_params_alloca(&swparams);
snd_pcm_sw_params_current(handle, swparams);
snd_pcm_sw_params_set_start_threshold(handle,swparams,period_frames);
snd_pcm_sw_params_set_stop_threshold(handle,swparams,buffer_frames);
snd_pcm_sw_params_set_avail_min(handle, swparams,period_frames);
snd_pcm_sw_params(handle, swparams);
// Step 4: Prepare audio
if(audioBuffer == 0)
audioBuffer = new char[snd_pcm_frames_to_bytes(handle,buffer_frames)];
snd_pcm_prepare( handle );
snd_pcm_start(handle);
// Step 5: Setup timer
bytesAvailable = bytesFree();
// Step 6: Start audio processing
timer->start(period_time/1000);
clockStamp.restart();
timeStamp.restart();
elapsedTimeOffset = 0;
errorState = QAudio::NoError;
totalTimeValue = 0;
opened = true;
return true;
}
void QAlsaAudioOutput::close()
{
timer->stop();
if ( handle ) {
snd_pcm_drain( handle );
snd_pcm_close( handle );
handle = 0;
delete [] audioBuffer;
audioBuffer=0;
}
if(!pullMode && audioSource) {
delete audioSource;
audioSource = 0;
}
opened = false;
}
int QAlsaAudioOutput::bytesFree() const
{
if(resuming)
return period_size;
if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
return 0;
int frames = snd_pcm_avail_update(handle);
if (frames == -EPIPE) {
// Try and handle buffer underrun
int err = snd_pcm_recover(handle, frames, 0);
if (err < 0)
return 0;
else
frames = snd_pcm_avail_update(handle);
} else if (frames < 0) {
return 0;
}
if ((int)frames > (int)buffer_frames)
frames = buffer_frames;
return snd_pcm_frames_to_bytes(handle, frames);
}
qint64 QAlsaAudioOutput::write( const char *data, qint64 len )
{
// Write out some audio data
if ( !handle )
return 0;
#ifdef DEBUG_AUDIO
qDebug()<<"frames to write out = "<<
snd_pcm_bytes_to_frames( handle, (int)len )<<" ("<<len<<") bytes";
#endif
int frames, err;
int space = bytesFree();
if (!space)
return 0;
if (len < space)
space = len;
frames = snd_pcm_bytes_to_frames(handle, space);
if (m_volume < 1.0f) {
QVarLengthArray<char, 4096> out(space);
QAudioHelperInternal::qMultiplySamples(m_volume, settings, data, out.data(), space);
err = snd_pcm_writei(handle, out.constData(), frames);
} else {
err = snd_pcm_writei(handle, data, frames);
}
if(err > 0) {
totalTimeValue += err;
resuming = false;
errorState = QAudio::NoError;
if (deviceState != QAudio::ActiveState) {
deviceState = QAudio::ActiveState;
emit stateChanged(deviceState);
}
return snd_pcm_frames_to_bytes( handle, err );
} else
err = xrun_recovery(err);
if(err < 0) {
close();
errorState = QAudio::FatalError;
emit errorChanged(errorState);
deviceState = QAudio::StoppedState;
emit stateChanged(deviceState);
}
return 0;
}
int QAlsaAudioOutput::periodSize() const
{
return period_size;
}
void QAlsaAudioOutput::setBufferSize(int value)
{
if(deviceState == QAudio::StoppedState)
buffer_size = value;
}
int QAlsaAudioOutput::bufferSize() const
{
return buffer_size;
}
void QAlsaAudioOutput::setNotifyInterval(int ms)
{
intervalTime = qMax(0, ms);
}
int QAlsaAudioOutput::notifyInterval() const
{
return intervalTime;
}
qint64 QAlsaAudioOutput::processedUSecs() const
{
return qint64(1000000) * totalTimeValue / settings.sampleRate();
}
void QAlsaAudioOutput::resume()
{
if(deviceState == QAudio::SuspendedState) {
int err = 0;
if(handle) {
err = snd_pcm_prepare( handle );
if(err < 0)
xrun_recovery(err);
err = snd_pcm_start(handle);
if(err < 0)
xrun_recovery(err);
bytesAvailable = (int)snd_pcm_frames_to_bytes(handle, buffer_frames);
}
resuming = true;
deviceState = pullMode ? QAudio::ActiveState : QAudio::IdleState;
errorState = QAudio::NoError;
timer->start(period_time/1000);
emit stateChanged(deviceState);
}
}
void QAlsaAudioOutput::setFormat(const QAudioFormat& fmt)
{
if (deviceState == QAudio::StoppedState)
settings = fmt;
}
QAudioFormat QAlsaAudioOutput::format() const
{
return settings;
}
void QAlsaAudioOutput::suspend()
{
if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState || resuming) {
snd_pcm_drain(handle);
timer->stop();
deviceState = QAudio::SuspendedState;
errorState = QAudio::NoError;
emit stateChanged(deviceState);
}
}
void QAlsaAudioOutput::userFeed()
{
if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState)
return;
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :userFeed() OUT";
#endif
if(deviceState == QAudio::IdleState)
bytesAvailable = bytesFree();
deviceReady();
}
bool QAlsaAudioOutput::deviceReady()
{
if(pullMode) {
int l = 0;
int chunks = bytesAvailable/period_size;
if(chunks==0) {
bytesAvailable = bytesFree();
return false;
}
#ifdef DEBUG_AUDIO
qDebug()<<"deviceReady() avail="<<bytesAvailable<<" bytes, period size="<<period_size<<" bytes";
qDebug()<<"deviceReady() no. of chunks that can fit ="<<chunks<<", chunks in bytes ="<<period_size*chunks;
#endif
int input = period_frames*chunks;
if(input > (int)buffer_frames)
input = buffer_frames;
l = audioSource->read(audioBuffer,snd_pcm_frames_to_bytes(handle, input));
// reading can take a while and stream may have been stopped
if (!handle)
return false;
if(l > 0) {
// Got some data to output
if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
return true;
qint64 bytesWritten = write(audioBuffer,l);
if (bytesWritten != l)
audioSource->seek(audioSource->pos()-(l-bytesWritten));
bytesAvailable = bytesFree();
} else if(l == 0) {
// Did not get any data to output
bytesAvailable = bytesFree();
if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) {
// Underrun
if (deviceState != QAudio::IdleState) {
errorState = QAudio::UnderrunError;
emit errorChanged(errorState);
deviceState = QAudio::IdleState;
emit stateChanged(deviceState);
}
}
} else if(l < 0) {
close();
deviceState = QAudio::StoppedState;
errorState = QAudio::IOError;
emit errorChanged(errorState);
emit stateChanged(deviceState);
}
} else {
bytesAvailable = bytesFree();
if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) {
// Underrun
if (deviceState != QAudio::IdleState) {
errorState = QAudio::UnderrunError;
emit errorChanged(errorState);
deviceState = QAudio::IdleState;
emit stateChanged(deviceState);
}
}
}
if(deviceState != QAudio::ActiveState)
return true;
if(intervalTime && (timeStamp.elapsed() + elapsedTimeOffset) > intervalTime) {
emit notify();
elapsedTimeOffset = timeStamp.elapsed() + elapsedTimeOffset - intervalTime;
timeStamp.restart();
}
return true;
}
qint64 QAlsaAudioOutput::elapsedUSecs() const
{
if (deviceState == QAudio::StoppedState)
return 0;
return clockStamp.elapsed() * qint64(1000);
}
void QAlsaAudioOutput::reset()
{
if(handle)
snd_pcm_reset(handle);
stop();
}
AlsaOutputPrivate::AlsaOutputPrivate(QAlsaAudioOutput* audio)
{
audioDevice = qobject_cast<QAlsaAudioOutput*>(audio);
}
AlsaOutputPrivate::~AlsaOutputPrivate() {}
qint64 AlsaOutputPrivate::readData( char* data, qint64 len)
{
Q_UNUSED(data)
Q_UNUSED(len)
return 0;
}
qint64 AlsaOutputPrivate::writeData(const char* data, qint64 len)
{
int retry = 0;
qint64 written = 0;
if((audioDevice->deviceState == QAudio::ActiveState)
||(audioDevice->deviceState == QAudio::IdleState)) {
while(written < len) {
int chunk = audioDevice->write(data+written,(len-written));
if(chunk <= 0)
retry++;
written+=chunk;
if(retry > 10)
return written;
}
}
return written;
}
QT_END_NAMESPACE
#include "moc_qalsaaudiooutput.cpp"