Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include <QtCore/qcoreapplication.h>
#include <QtCore/qvarlengtharray.h>
#include <QtMultimedia/private/qaudiohelpers_p.h>
#include "qalsaaudiosink_p.h"
#include <QLoggingCategory>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcAlsaOutput, "qt.multimedia.alsa.output");
//#define DEBUG_AUDIO 1
QAlsaAudioSink::QAlsaAudioSink(QAudioDevice device, const QAudioFormat &format, QObject *parent)
: QPlatformAudioSink(std::move(device), format, parent)
{
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed);
}
QAlsaAudioSink::~QAlsaAudioSink()
{
close();
disconnect(timer, &QTimer::timeout, this, &QAlsaAudioSink::userFeed);
delete timer;
}
QAudio::State QAlsaAudioSink::state() const
{
return deviceState;
}
int QAlsaAudioSink::xrun_recovery(int err)
{
int count = 0;
bool reset = false;
// ESTRPIPE is not available in all OSes where ALSA is available
int estrpipe = EIO;
#ifdef ESTRPIPE
estrpipe = ESTRPIPE;
#endif
if(err == -EPIPE) {
errorState = QAudio::UnderrunError;
setError(errorState);
err = snd_pcm_prepare(handle);
if(err < 0)
reset = true;
} else if ((err == -estrpipe)||(err == -EIO)) {
errorState = QAudio::IOError;
setError(errorState);
while ((err = snd_pcm_resume(handle)) == -EAGAIN) {
usleep(100);
count++;
if(count > 5) {
reset = true;
break;
}
}
if(err < 0) {
err = snd_pcm_prepare(handle);
if(err < 0)
reset = true;
}
}
if(reset) {
close();
open();
snd_pcm_prepare(handle);
return 0;
}
return err;
}
int QAlsaAudioSink::setFormat()
{
snd_pcm_format_t pcmformat = SND_PCM_FORMAT_UNKNOWN;
switch (m_format.sampleFormat()) {
case QAudioFormat::UInt8:
pcmformat = SND_PCM_FORMAT_U8;
break;
case QAudioFormat::Int16:
if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
pcmformat = SND_PCM_FORMAT_S16_BE;
else
pcmformat = SND_PCM_FORMAT_S16_LE;
break;
case QAudioFormat::Int32:
if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
pcmformat = SND_PCM_FORMAT_S32_BE;
else
pcmformat = SND_PCM_FORMAT_S32_LE;
break;
case QAudioFormat::Float:
if constexpr (QSysInfo::ByteOrder == QSysInfo::BigEndian)
pcmformat = SND_PCM_FORMAT_FLOAT_BE;
else
pcmformat = SND_PCM_FORMAT_FLOAT_LE;
break;
default:
break;
}
return pcmformat != SND_PCM_FORMAT_UNKNOWN
? snd_pcm_hw_params_set_format( handle, hwparams, pcmformat)
: -1;
}
void QAlsaAudioSink::start(QIODevice* device)
{
if(deviceState != QAudio::StoppedState)
deviceState = QAudio::StoppedState;
errorState = QAudio::NoError;
// Handle change of mode
if(audioSource && !pullMode) {
delete audioSource;
audioSource = 0;
}
close();
pullMode = true;
audioSource = device;
connect(audioSource, &QIODevice::readyRead, timer, [this] {
if (!timer->isActive()) {
timer->start(period_time / 1000);
}
});
deviceState = QAudio::ActiveState;
open();
Q_EMIT stateChanged(deviceState);
}
QIODevice* QAlsaAudioSink::start()
{
if(deviceState != QAudio::StoppedState)
deviceState = QAudio::StoppedState;
errorState = QAudio::NoError;
// Handle change of mode
if(audioSource && !pullMode) {
delete audioSource;
audioSource = 0;
}
close();
audioSource = new AlsaOutputPrivate(this);
audioSource->open(QIODevice::WriteOnly|QIODevice::Unbuffered);
pullMode = false;
deviceState = QAudio::IdleState;
open();
Q_EMIT stateChanged(deviceState);
return audioSource;
}
void QAlsaAudioSink::stop()
{
if(deviceState == QAudio::StoppedState)
return;
errorState = QAudio::NoError;
deviceState = QAudio::StoppedState;
close();
Q_EMIT stateChanged(deviceState);
}
bool QAlsaAudioSink::open()
{
if(opened)
return true;
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :open()";
#endif
elapsedTimeOffset = 0;
int dir;
int err = -1;
int count=0;
unsigned int sampleRate = m_format.sampleRate();
// Step 1: try and open the device
while((count < 5) && (err < 0)) {
err = snd_pcm_open(&handle, m_audioDevice.id().constData(), SND_PCM_STREAM_PLAYBACK, 0);
if(err < 0)
count++;
}
if (( err < 0)||(handle == 0)) {
errorState = QAudio::OpenError;
setError(errorState);
deviceState = QAudio::StoppedState;
return false;
}
snd_pcm_nonblock( handle, 0 );
// Step 2: Set the desired HW parameters.
snd_pcm_hw_params_alloca( &hwparams );
bool fatal = false;
QString errMessage;
unsigned int chunks = 8;
err = snd_pcm_hw_params_any( handle, hwparams );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_any: err = %1").arg(err);
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_rate_resample( handle, hwparams, 1 );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_rate_resample: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_access( handle, hwparams, access );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_access: err = %1").arg(err);
}
}
if ( !fatal ) {
err = setFormat();
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_format: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_channels(handle, hwparams,
(unsigned int)m_format.channelCount());
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_channels: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_rate_near( handle, hwparams, &sampleRate, 0 );
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_rate_near: err = %1").arg(err);
}
}
if ( !fatal ) {
unsigned int maxBufferTime = 0;
unsigned int minBufferTime = 0;
unsigned int maxPeriodTime = 0;
unsigned int minPeriodTime = 0;
err = snd_pcm_hw_params_get_buffer_time_max(hwparams, &maxBufferTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_buffer_time_min(hwparams, &minBufferTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_period_time_max(hwparams, &maxPeriodTime, &dir);
if ( err >= 0)
err = snd_pcm_hw_params_get_period_time_min(hwparams, &minPeriodTime, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: buffer/period min and max: err = %1").arg(err);
} else {
static unsigned user_buffer_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_BUFFER_TIME");
static unsigned user_period_time = qEnvironmentVariableIntValue("QT_ALSA_OUTPUT_PERIOD_TIME");
const bool outOfRange = maxBufferTime < buffer_time || buffer_time < minBufferTime || maxPeriodTime < period_time || minPeriodTime > period_time;
if (outOfRange || user_period_time || user_buffer_time) {
period_time = user_period_time ? user_period_time : minPeriodTime;
if (!user_buffer_time) {
chunks = maxBufferTime / period_time;
buffer_time = period_time * chunks;
} else {
buffer_time = user_buffer_time;
chunks = buffer_time / period_time;
}
}
qCDebug(lcAlsaOutput) << "buffer time: [" << minBufferTime << "-" << maxBufferTime << "] =" << buffer_time;
qCDebug(lcAlsaOutput) << "period time: [" << minPeriodTime << "-" << maxPeriodTime << "] =" << period_time;
qCDebug(lcAlsaOutput) << "chunks =" << chunks;
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_buffer_time_near(handle, hwparams, &buffer_time, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_buffer_time_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_period_time_near(handle, hwparams, &period_time, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_period_time_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &chunks, &dir);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params_set_periods_near: err = %1").arg(err);
}
}
if ( !fatal ) {
err = snd_pcm_hw_params(handle, hwparams);
if ( err < 0 ) {
fatal = true;
errMessage = QString::fromLatin1("QAudioSink: snd_pcm_hw_params: err = %1").arg(err);
}
}
if( err < 0) {
qWarning()<<errMessage;
errorState = QAudio::OpenError;
setError(errorState);
deviceState = QAudio::StoppedState;
return false;
}
snd_pcm_hw_params_get_buffer_size(hwparams,&buffer_frames);
buffer_size = snd_pcm_frames_to_bytes(handle,buffer_frames);
snd_pcm_hw_params_get_period_size(hwparams,&period_frames, &dir);
period_size = snd_pcm_frames_to_bytes(handle,period_frames);
snd_pcm_hw_params_get_buffer_time(hwparams,&buffer_time, &dir);
snd_pcm_hw_params_get_period_time(hwparams,&period_time, &dir);
// Step 3: Set the desired SW parameters.
snd_pcm_sw_params_t *swparams;
snd_pcm_sw_params_alloca(&swparams);
snd_pcm_sw_params_current(handle, swparams);
snd_pcm_sw_params_set_start_threshold(handle,swparams,period_frames);
snd_pcm_sw_params_set_stop_threshold(handle,swparams,buffer_frames);
snd_pcm_sw_params_set_avail_min(handle, swparams,period_frames);
snd_pcm_sw_params(handle, swparams);
// Step 4: Prepare audio
if(audioBuffer == 0)
audioBuffer = new char[snd_pcm_frames_to_bytes(handle,buffer_frames)];
snd_pcm_prepare( handle );
snd_pcm_start(handle);
// Step 5: Setup timer
bytesAvailable = bytesFree();
// Step 6: Start audio processing
timer->start(period_time/1000);
elapsedTimeOffset = 0;
errorState = QAudio::NoError;
totalTimeValue = 0;
opened = true;
return true;
}
void QAlsaAudioSink::close()
{
timer->stop();
if ( handle ) {
snd_pcm_drain( handle );
snd_pcm_close( handle );
handle = 0;
delete [] audioBuffer;
audioBuffer=0;
}
if(!pullMode && audioSource) {
delete audioSource;
audioSource = 0;
}
opened = false;
}
qsizetype QAlsaAudioSink::bytesFree() const
{
if(resuming)
return period_size;
if(deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
return 0;
int frames = snd_pcm_avail_update(handle);
if (frames == -EPIPE) {
// Try and handle buffer underrun
int err = snd_pcm_recover(handle, frames, 0);
if (err < 0)
return 0;
else
frames = snd_pcm_avail_update(handle);
} else if (frames < 0) {
return 0;
}
if ((int)frames > (int)buffer_frames)
frames = buffer_frames;
return snd_pcm_frames_to_bytes(handle, frames);
}
qint64 QAlsaAudioSink::write( const char *data, qint64 len )
{
// Write out some audio data
if ( !handle )
return 0;
#ifdef DEBUG_AUDIO
qDebug()<<"frames to write out = "<<
snd_pcm_bytes_to_frames( handle, (int)len )<<" ("<<len<<") bytes";
#endif
int frames, err;
int space = bytesFree();
if (!space)
return 0;
if (len < space)
space = len;
frames = snd_pcm_bytes_to_frames(handle, space);
if (volume() < 1.0f) {
QVarLengthArray<char, 4096> out(space);
QAudioHelperInternal::qMultiplySamples(volume(), m_format, 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;
Q_EMIT stateChanged(deviceState);
}
return snd_pcm_frames_to_bytes( handle, err );
} else
err = xrun_recovery(err);
if(err < 0) {
close();
errorState = QAudio::FatalError;
setError(errorState);
deviceState = QAudio::StoppedState;
Q_EMIT stateChanged(deviceState);
}
return 0;
}
void QAlsaAudioSink::setBufferSize(qsizetype value)
{
if(deviceState == QAudio::StoppedState)
buffer_size = value;
}
qsizetype QAlsaAudioSink::bufferSize() const
{
return buffer_size;
}
qint64 QAlsaAudioSink::processedUSecs() const
{
return qint64(1000000) * totalTimeValue / m_format.sampleRate();
}
void QAlsaAudioSink::resume()
{
if(deviceState == QAudio::SuspendedState) {
int err = 0;
if(handle) {
err = snd_pcm_prepare( handle );
if(err < 0)
xrun_recovery(err);
err = snd_pcm_start(handle);
if(err < 0)
xrun_recovery(err);
bytesAvailable = (int)snd_pcm_frames_to_bytes(handle, buffer_frames);
}
resuming = true;
deviceState = suspendedInState;
errorState = QAudio::NoError;
timer->start(period_time/1000);
Q_EMIT stateChanged(deviceState);
}
}
void QAlsaAudioSink::suspend()
{
if(deviceState == QAudio::ActiveState || deviceState == QAudio::IdleState || resuming) {
suspendedInState = deviceState;
snd_pcm_drain(handle);
timer->stop();
deviceState = QAudio::SuspendedState;
errorState = QAudio::NoError;
Q_EMIT stateChanged(deviceState);
}
}
void QAlsaAudioSink::userFeed()
{
if(deviceState == QAudio::StoppedState || deviceState == QAudio::SuspendedState)
return;
#ifdef DEBUG_AUDIO
QTime now(QTime::currentTime());
qDebug()<<now.second()<<"s "<<now.msec()<<"ms :userFeed() OUT";
#endif
if(deviceState == QAudio::IdleState)
bytesAvailable = bytesFree();
deviceReady();
}
bool QAlsaAudioSink::deviceReady()
{
if(pullMode) {
int l = 0;
int chunks = bytesAvailable/period_size;
if(chunks==0) {
bytesAvailable = bytesFree();
return false;
}
#ifdef DEBUG_AUDIO
qDebug()<<"deviceReady() avail="<<bytesAvailable<<" bytes, period size="<<period_size<<" bytes";
qDebug()<<"deviceReady() no. of chunks that can fit ="<<chunks<<", chunks in bytes ="<<period_size*chunks;
#endif
int input = period_frames*chunks;
if(input > (int)buffer_frames)
input = buffer_frames;
l = audioSource->read(audioBuffer,snd_pcm_frames_to_bytes(handle, input));
// reading can take a while and stream may have been stopped
if (!handle)
return false;
if(l > 0) {
// Got some data to output
if (deviceState != QAudio::ActiveState && deviceState != QAudio::IdleState)
return true;
qint64 bytesWritten = write(audioBuffer,l);
if (bytesWritten != l)
audioSource->seek(audioSource->pos()-(l-bytesWritten));
bytesAvailable = bytesFree();
} else if(l == 0) {
// Did not get any data to output
timer->stop();
snd_pcm_drain(handle);
bytesAvailable = bytesFree();
if(bytesAvailable > snd_pcm_frames_to_bytes(handle, buffer_frames-period_frames)) {
// Underrun
if (deviceState != QAudio::IdleState) {
errorState = audioSource->atEnd() ? QAudio::NoError : QAudio::UnderrunError;
setError(errorState);
deviceState = QAudio::IdleState;
Q_EMIT stateChanged(deviceState);
}
}
} else if(l < 0) {
close();
deviceState = QAudio::StoppedState;
errorState = QAudio::IOError;
setError(errorState);
Q_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;
setError(errorState);
deviceState = QAudio::IdleState;
Q_EMIT stateChanged(deviceState);
}
}
}
if(deviceState != QAudio::ActiveState)
return true;
return true;
}
void QAlsaAudioSink::reset()
{
if(handle)
snd_pcm_reset(handle);
stop();
}
AlsaOutputPrivate::AlsaOutputPrivate(QAlsaAudioSink* audio)
{
audioDevice = qobject_cast<QAlsaAudioSink*>(audio);
}
AlsaOutputPrivate::~AlsaOutputPrivate() {}
qint64 AlsaOutputPrivate::readData( char* data, qint64 len)
{
Q_UNUSED(data);
Q_UNUSED(len);
return 0;
}
qint64 AlsaOutputPrivate::writeData(const char* data, qint64 len)
{
int retry = 0;
qint64 written = 0;
if((audioDevice->deviceState == QAudio::ActiveState)
||(audioDevice->deviceState == QAudio::IdleState)) {
while(written < len) {
int chunk = audioDevice->write(data+written,(len-written));
if(chunk <= 0)
retry++;
written+=chunk;
if(retry > 10)
return written;
}
}
return written;
}
QT_END_NAMESPACE
#include "moc_qalsaaudiosink_p.cpp"