|
|
|
/*************************************************************************** |
|
* Copyright (C) 2007 by John Stamp * |
|
* jstamp@users.sourceforge.net * |
|
* * |
|
* Large portions of this code are shamelessly copied from audio.c: * |
|
* The XMMS ALSA output plugin * |
|
* Copyright (C) 2001-2003 Matthieu Sozeau <mattam@altern.org> * |
|
* Copyright (C) 1998-2003 Peter Alm, Mikael Alm, Olle Hallnas, * |
|
* Thomas Nilsson and 4Front Technologies * |
|
* Copyright (C) 1999-2006 Haavard Kvaalen * |
|
* Copyright (C) 2005 Takashi Iwai * |
|
* * |
|
* This program is free software; you can redistribute it and/or modify * |
|
* it under the terms of the GNU General Public License as published by * |
|
* the Free Software Foundation; either version 2 of the License, or * |
|
* (at your option) any later version. * |
|
* * |
|
* This program is distributed in the hope that it will be useful, * |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of * |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
|
* GNU General Public License for more details. * |
|
* * |
|
* You should have received a copy of the GNU General Public License * |
|
* along with this program; if not, write to the * |
|
* Free Software Foundation, Inc., * |
|
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * |
|
***************************************************************************/ |
|
#include "alsaaudio.h" |
|
#include "Loqqer.h" |
|
#include <qendian.h> |
|
|
|
#include <QDebug> |
|
|
|
#define MIN( a, b ) ( ( ( a ) < ( b ) ) ? ( a ) : ( b ) ) |
|
#define MAX_BUFFER_SIZE 5 |
|
|
|
QMutex AlsaAudio::mutex; |
|
pthread_t AlsaAudio::audio_thread; |
|
|
|
QByteArray AlsaAudio::audioData; |
|
snd_output_t* AlsaAudio::logs = NULL; |
|
bool AlsaAudio::going = false; |
|
snd_pcm_t *AlsaAudio::alsa_pcm = NULL; |
|
|
|
int AlsaAudio::hw_period_size_in = 0; |
|
snd_format* AlsaAudio::inputf = NULL; |
|
snd_format* AlsaAudio::outputf = NULL; |
|
float AlsaAudio::volume = 1.0; |
|
|
|
convert_func_t AlsaAudio::alsa_convert_func = NULL; |
|
convert_channel_func_t AlsaAudio::alsa_stereo_convert_func =NULL; |
|
convert_freq_func_t AlsaAudio::alsa_frequency_convert_func =NULL; |
|
xmms_convert_buffers *AlsaAudio::convertb = NULL; |
|
bool AlsaAudio::use_mmap = false; |
|
|
|
|
|
AlsaAudio::AlsaAudio() |
|
{ |
|
maxBufferSize = 0; |
|
} |
|
|
|
|
|
AlsaAudio::~AlsaAudio() |
|
{ |
|
} |
|
|
|
|
|
/****************************************************************************** |
|
Device Detection |
|
******************************************************************************/ |
|
|
|
int AlsaAudio::getCards( void ) |
|
{ |
|
int card = -1; |
|
int err = 0; |
|
_devices.clear(); |
|
|
|
if ( ( err = snd_card_next( &card ) ) != 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::getCards() failed: " << snd_strerror( -err ) ); |
|
return -1; |
|
} |
|
|
|
while ( card > -1 ) |
|
{ |
|
getDevicesForCard( card ); |
|
if ( ( err = snd_card_next( &card ) ) != 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::getCards() failed: " << snd_strerror( -err ) ); |
|
return -1; |
|
} |
|
} |
|
return _devices.size(); |
|
} |
|
|
|
|
|
void AlsaAudio::getDevicesForCard( int card ) |
|
{ |
|
int pcm_device = -1, err; |
|
snd_pcm_info_t *pcm_info; |
|
snd_ctl_t *ctl; |
|
char devName[64], *card_name; |
|
|
|
sprintf( devName, "hw:%i", card ); |
|
|
|
if ( ( err = snd_ctl_open( &ctl, devName, 0 ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::getDevicesForCard() failed: " << snd_strerror( -err ) ); |
|
return; |
|
} |
|
|
|
if ( ( err = snd_card_get_name( card, &card_name ) ) != 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::getDevicesForCard() failed: " << snd_strerror( -err ) ); |
|
card_name = "Unknown soundcard"; |
|
} |
|
|
|
// Each card has its own default device |
|
// But test, just to be sure it's there |
|
AlsaDeviceInfo dev; |
|
dev.name = QString( "%1: Default Device (default:%2)" ).arg( card_name ).arg( card ); |
|
dev.device = QString( "default:%1" ).arg( card ); |
|
snd_pcm_t *test_pcm; |
|
err = snd_pcm_open( &test_pcm, dev.device.toStdString().c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK ); |
|
if ( err >= 0 ) |
|
snd_pcm_close( test_pcm ); |
|
if ( err == 0 || err == -EBUSY ) |
|
_devices.push_back( dev ); |
|
|
|
snd_pcm_info_alloca( &pcm_info ); |
|
|
|
for ( ;; ) |
|
{ |
|
char device[64], descr[128]; |
|
if ( ( err = snd_ctl_pcm_next_device( ctl, &pcm_device ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::getDevicesForCard() failed: " << snd_strerror( -err ) ); |
|
pcm_device = -1; |
|
} |
|
if ( pcm_device < 0 ) |
|
break; |
|
|
|
snd_pcm_info_set_device( pcm_info, pcm_device ); |
|
snd_pcm_info_set_subdevice( pcm_info, 0 ); |
|
snd_pcm_info_set_stream( pcm_info, SND_PCM_STREAM_PLAYBACK ); |
|
|
|
if ( ( err = snd_ctl_pcm_info( ctl, pcm_info ) ) < 0 ) |
|
{ |
|
if ( err != -ENOENT ) |
|
LOGL( 1, "AlsaAudio::getDevicesForCard: snd_ctl_pcm_info() failed (" |
|
<< card << ":" << pcm_device << "): " << snd_strerror( -err ) ); |
|
continue; |
|
} |
|
|
|
sprintf( device, "hw:%d,%d", card, pcm_device ); |
|
sprintf( descr, "%s: %s (%s)", card_name, |
|
snd_pcm_info_get_name( pcm_info ), |
|
device ); |
|
dev.name = descr; |
|
dev.device = device; |
|
_devices.push_back( dev ); |
|
} |
|
|
|
snd_ctl_close( ctl ); |
|
} |
|
|
|
|
|
AlsaDeviceInfo AlsaAudio::getDeviceInfo( int device ) |
|
{ |
|
return _devices[device]; |
|
} |
|
|
|
|
|
/****************************************************************************** |
|
Device Setup |
|
******************************************************************************/ |
|
|
|
bool AlsaAudio::alsaSetup( QString device, snd_pcm_uframes_t periodSize, uint periodCount, snd_format *f ) |
|
{ |
|
int err, hw_period_size; |
|
snd_pcm_hw_params_t *hwparams; |
|
snd_pcm_sw_params_t *swparams; |
|
snd_pcm_uframes_t alsa_buffer_size, alsa_period_size; |
|
snd_pcm_access_mask_t *mask; |
|
|
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "AlsaAudio::alsaSetup()"; |
|
snd_output_stdio_attach( &logs, stderr, 0 ); |
|
#endif |
|
|
|
alsa_convert_func = NULL; |
|
alsa_stereo_convert_func = NULL; |
|
alsa_frequency_convert_func = NULL; |
|
|
|
free( outputf ); |
|
outputf = snd_format_from_xmms( f->xmms_format, f->rate, f->channels ); |
|
|
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Opening device:" << device; |
|
#endif |
|
// FIXME: Can snd_pcm_open() return EAGAIN? |
|
if ( ( err = snd_pcm_open( &alsa_pcm, device.toStdString().c_str(), |
|
SND_PCM_STREAM_PLAYBACK, |
|
SND_PCM_NONBLOCK ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Failed to open pcm device (" << device << "): " << snd_strerror( -err ) ); |
|
alsa_pcm = NULL; |
|
free( outputf ); |
|
outputf = NULL; |
|
return false; |
|
} |
|
|
|
#ifndef QT_NO_DEBUG |
|
snd_pcm_info_t *info; |
|
int alsa_card, alsa_device, alsa_subdevice; |
|
|
|
snd_pcm_info_alloca( &info ); |
|
snd_pcm_info( alsa_pcm, info ); |
|
alsa_card = snd_pcm_info_get_card( info ); |
|
alsa_device = snd_pcm_info_get_device( info ); |
|
alsa_subdevice = snd_pcm_info_get_subdevice( info ); |
|
qDebug() << "Card:" << alsa_card << "Device:" << alsa_device << "Subdevice:" << alsa_subdevice; |
|
#endif |
|
|
|
snd_pcm_hw_params_alloca( &hwparams ); |
|
|
|
if ( ( err = snd_pcm_hw_params_any( alsa_pcm, hwparams ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): No configuration available for playback: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
|
|
// First try to set up mmapped access |
|
mask = (snd_pcm_access_mask_t*)alloca(snd_pcm_access_mask_sizeof()); |
|
snd_pcm_access_mask_none(mask); |
|
snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_INTERLEAVED); |
|
snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_NONINTERLEAVED); |
|
snd_pcm_access_mask_set(mask, SND_PCM_ACCESS_MMAP_COMPLEX); |
|
|
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Trying to set mmapped write mode"; |
|
#endif |
|
if ( ( err = snd_pcm_hw_params_set_access_mask( alsa_pcm, hwparams, mask ) ) < 0 ) |
|
{ |
|
use_mmap = false; |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Setting mmapped write mode failed: " << snd_strerror( -err ) << "\n Trying normal write mode"; |
|
#endif |
|
if ( ( err = snd_pcm_hw_params_set_access( alsa_pcm, hwparams, |
|
SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Cannot set normal write mode: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
} |
|
else |
|
use_mmap = true; |
|
|
|
if ( ( err = snd_pcm_hw_params_set_format( alsa_pcm, hwparams, outputf->format ) ) < 0 ) |
|
{ |
|
//Try if one of these format work (one of them should work |
|
//on almost all soundcards) |
|
|
|
snd_pcm_format_t formats[] = { SND_PCM_FORMAT_S16_LE, |
|
SND_PCM_FORMAT_S16_BE, |
|
SND_PCM_FORMAT_U8 |
|
}; |
|
uint i; |
|
|
|
for ( i = 0; i < sizeof( formats ) / sizeof( formats[0] ); i++ ) |
|
{ |
|
if ( snd_pcm_hw_params_set_format( alsa_pcm, hwparams, formats[i] ) == 0 ) |
|
{ |
|
outputf->format = formats[i]; |
|
break; |
|
} |
|
} |
|
if ( outputf->format != f->format ) |
|
{ |
|
outputf->xmms_format = (AFormat)format_from_alsa( outputf->format ); |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Converting format from" << f->xmms_format << "to" << outputf->xmms_format; |
|
#endif |
|
if ( outputf->xmms_format < 0 ) |
|
return -1; |
|
alsa_convert_func = xmms_convert_get_func( outputf->xmms_format, f->xmms_format ); |
|
if ( alsa_convert_func == NULL ) |
|
{ |
|
LOGL( 1, "Format translation needed, but not available. Input: " << f->xmms_format << "; Output: " << outputf->xmms_format ); |
|
return false; |
|
} |
|
} |
|
else |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Sample format not available for playback: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
} |
|
|
|
snd_pcm_hw_params_set_channels_near( alsa_pcm, hwparams, &outputf->channels ); |
|
if ( outputf->channels != f->channels ) |
|
{ |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Converting channels from" << f->channels << "to" << outputf->channels; |
|
#endif |
|
alsa_stereo_convert_func = |
|
xmms_convert_get_channel_func( outputf->xmms_format, |
|
outputf->channels, |
|
f->channels ); |
|
if ( alsa_stereo_convert_func == NULL ) |
|
{ |
|
LOGL( 1, "No stereo conversion available. Format: " << outputf->xmms_format << "; Input Channels: " << f->channels << "; Output Channels: " << outputf->channels ); |
|
return false; |
|
} |
|
} |
|
|
|
snd_pcm_hw_params_set_rate_near( alsa_pcm, hwparams, &outputf->rate, 0 ); |
|
if ( outputf->rate == 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup: No usable samplerate available." ); |
|
return false; |
|
} |
|
if ( outputf->rate != f->rate ) |
|
{ |
|
LOGL( 3, "Converting samplerate from " << f->rate << " to " << outputf->rate ); |
|
if ( outputf->channels < 1 || outputf->channels > 2 ) |
|
{ |
|
LOGL( 1, "Unsupported number of channels: " << outputf->channels << ". Resample function not available" ); |
|
alsa_frequency_convert_func = NULL; |
|
return false; |
|
} |
|
alsa_frequency_convert_func = |
|
xmms_convert_get_frequency_func( outputf->xmms_format, |
|
outputf->channels ); |
|
if ( alsa_frequency_convert_func == NULL ) |
|
{ |
|
LOGL( 1, "Resample function not available. Format " << outputf->xmms_format ); |
|
return false; |
|
} |
|
} |
|
|
|
outputf->sample_bits = snd_pcm_format_physical_width( outputf->format ); |
|
outputf->bps = ( outputf->rate * outputf->sample_bits * outputf->channels ) >> 3; |
|
|
|
if ( ( err = snd_pcm_hw_params_set_period_size_near( alsa_pcm, hwparams, |
|
&periodSize, NULL ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Set period size failed: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
|
|
if ( ( err = snd_pcm_hw_params_set_periods_near( alsa_pcm, hwparams, |
|
&periodCount, 0 ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Set period count failed: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
|
|
if ( snd_pcm_hw_params( alsa_pcm, hwparams ) < 0 ) |
|
{ |
|
#ifndef QT_NO_DEBUG |
|
snd_pcm_hw_params_dump( hwparams, logs ); |
|
#endif |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Unable to install hw params" ); |
|
return false; |
|
} |
|
|
|
if ( ( err = snd_pcm_hw_params_get_buffer_size( hwparams, &alsa_buffer_size ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): snd_pcm_hw_params_get_buffer_size() failed: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
|
|
if ( ( err = snd_pcm_hw_params_get_period_size( hwparams, &alsa_period_size, 0 ) ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): snd_pcm_hw_params_get_period_size() failed: " << snd_strerror( -err ) ); |
|
return false; |
|
} |
|
snd_pcm_sw_params_alloca( &swparams ); |
|
snd_pcm_sw_params_current( alsa_pcm, swparams ); |
|
|
|
if ( ( err = snd_pcm_sw_params_set_start_threshold( alsa_pcm, |
|
swparams, alsa_buffer_size - alsa_period_size ) < 0 ) ) |
|
LOGL( 1, "AlsaAudio::alsaSetup(): setting start threshold failed: " << snd_strerror( -err ) ); |
|
if ( snd_pcm_sw_params( alsa_pcm, swparams ) < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::alsaSetup(): Unable to install sw params" ); |
|
return false; |
|
} |
|
|
|
#ifndef QT_NO_DEBUG |
|
snd_pcm_sw_params_dump( swparams, logs ); |
|
snd_pcm_dump( alsa_pcm, logs ); |
|
#endif |
|
hw_period_size = snd_pcm_frames_to_bytes( alsa_pcm, alsa_period_size ); |
|
if ( inputf->bps != outputf->bps ) |
|
{ |
|
int align = ( inputf->sample_bits * inputf->channels ) / 8; |
|
hw_period_size_in = ( (quint64)hw_period_size * inputf->bps + |
|
outputf->bps/2 ) / outputf->bps; |
|
hw_period_size_in -= hw_period_size_in % align; |
|
} |
|
else |
|
{ |
|
hw_period_size_in = hw_period_size; |
|
} |
|
|
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Device setup: period size:" << hw_period_size; |
|
qDebug() << "bits per sample:" << snd_pcm_format_physical_width( outputf->format ) << |
|
"frame size:" << snd_pcm_frames_to_bytes( alsa_pcm, 1 ) << |
|
"Bps:" << outputf->bps; |
|
#endif |
|
return true; |
|
} |
|
|
|
bool AlsaAudio::alsaOpen( QString device, AFormat format, unsigned int rate, unsigned int channels, unsigned int buffer_time, unsigned int period_time ) |
|
{ |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Opening device"; |
|
#endif |
|
inputf = snd_format_from_xmms( format, rate, channels ); |
|
|
|
// We'll be using this in alsaWrite |
|
maxBufferSize = inputf->bps * MAX_BUFFER_SIZE; |
|
// And clear the buffer, just in case |
|
clearBuffer(); |
|
|
|
if ( alsaSetup( device, buffer_time, period_time, inputf ) == false ) |
|
{ |
|
alsaClose(); |
|
return false; |
|
} |
|
|
|
going = true; |
|
convertb = xmms_convert_buffers_new(); |
|
|
|
AlsaAudio* aaThread = new AlsaAudio(); |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Starting thread"; |
|
#endif |
|
pthread_create( &audio_thread, NULL, &alsa_loop, (void*)aaThread ); |
|
|
|
return true; |
|
} |
|
|
|
void AlsaAudio::clearBuffer( void ) |
|
{ |
|
mutex.lock(); |
|
audioData.clear(); |
|
mutex.unlock(); |
|
} |
|
|
|
/****************************************************************************** |
|
Play Interface |
|
******************************************************************************/ |
|
|
|
void AlsaAudio::alsaWrite( const QByteArray* inputData ) |
|
{ |
|
if ( ( audioData.size() + inputData->size() ) < maxBufferSize ) |
|
{ |
|
mutex.lock(); |
|
#ifndef QT_NO_DEBUG |
|
qDebug( "max buffer size: %d; buffer data: %d; input data: %d", maxBufferSize, audioData.size(), inputData->size() ); |
|
#endif |
|
audioData.append( *inputData ); |
|
mutex.unlock(); |
|
} |
|
else |
|
{ |
|
mutex.lock(); |
|
int inSize = maxBufferSize - audioData.size(); |
|
#ifndef QT_NO_DEBUG |
|
qDebug( "max buffer size: %d; buffer data: %d; input data: %d; truncated to: %d", |
|
maxBufferSize, audioData.size(), inputData->size(), inSize ); |
|
#endif |
|
audioData.append( inputData->left( inSize ) ); |
|
mutex.unlock(); |
|
LOGL( 1, "Max data buffer size reached. Bytes dropped: " << inSize ); |
|
} |
|
} |
|
|
|
|
|
int AlsaAudio::bufferSize( void ) |
|
{ |
|
return audioData.size(); |
|
} |
|
|
|
void AlsaAudio::setVolume ( float vol ) |
|
{ |
|
volume = vol; |
|
} |
|
|
|
|
|
void AlsaAudio::alsaClose( void ) |
|
{ |
|
if ( !going ) |
|
return; |
|
|
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Closing device"; |
|
#endif |
|
|
|
going = false; |
|
|
|
pthread_join( audio_thread, NULL ); |
|
|
|
xmms_convert_buffers_destroy( convertb ); |
|
convertb = NULL; |
|
free( inputf ); |
|
inputf = NULL; |
|
free( outputf ); |
|
outputf = NULL; |
|
|
|
#ifndef QT_NO_DEBUG |
|
snd_output_close( logs ); |
|
qDebug() << "Device closed"; |
|
#endif |
|
} |
|
|
|
|
|
/****************************************************************************** |
|
Play Thread |
|
******************************************************************************/ |
|
|
|
void* AlsaAudio::alsa_loop( void* pthis ) |
|
{ |
|
AlsaAudio* aaThread = (AlsaAudio*)pthis; |
|
aaThread->run(); |
|
return NULL; |
|
} |
|
|
|
|
|
void AlsaAudio::run( void ) |
|
{ |
|
int npfds = snd_pcm_poll_descriptors_count( alsa_pcm ); |
|
struct pollfd *pfds; |
|
unsigned short *revents; |
|
|
|
if ( npfds <= 0 ) |
|
goto _error; |
|
pfds = (struct pollfd*)malloc( sizeof( *pfds ) * npfds ); |
|
revents = (unsigned short*)malloc( sizeof( *revents ) * npfds ); |
|
while ( going && alsa_pcm ) |
|
{ |
|
if ( audioData.size() > hw_period_size_in ) |
|
{ |
|
snd_pcm_poll_descriptors( alsa_pcm, pfds, npfds ); |
|
if ( poll( pfds, npfds, 10 ) > 0 ) |
|
{ |
|
// need to check revents. poll() with |
|
// dmix returns a postive value even |
|
// if no data is available |
|
int i; |
|
snd_pcm_poll_descriptors_revents( alsa_pcm, pfds, |
|
npfds, revents ); |
|
for ( i = 0; i < npfds; i++ ) |
|
if ( revents[i] & POLLOUT ) |
|
{ |
|
pumpThreadData(); |
|
break; |
|
} |
|
} |
|
} |
|
else |
|
{ |
|
struct timespec req; |
|
req.tv_sec = 0; |
|
req.tv_nsec = 10000000; |
|
nanosleep( &req, NULL ); |
|
} |
|
} |
|
free( pfds ); |
|
free( revents ); |
|
|
|
_error: |
|
alsa_close_pcm(); |
|
mutex.lock(); |
|
audioData.clear(); |
|
mutex.unlock(); |
|
#ifndef QT_NO_DEBUG |
|
qDebug() << "Exiting thread"; |
|
#endif |
|
pthread_exit( NULL ); |
|
} |
|
|
|
|
|
/* transfer audio data from thread buffer to h/w */ |
|
void AlsaAudio::pumpThreadData( void ) |
|
{ |
|
int length, cnt, avail, datSize; |
|
|
|
datSize = audioData.size(); |
|
length = MIN( hw_period_size_in, datSize ); |
|
avail = snd_pcm_frames_to_bytes( alsa_pcm, getAvailableFrames() ); |
|
length = MIN( length, avail ); |
|
while ( length > 0 ) |
|
{ |
|
cnt = MIN( length, datSize ); |
|
convertData( audioData.left( cnt ).data(), cnt ); |
|
mutex.lock(); |
|
audioData.remove( 0, cnt ); |
|
mutex.unlock(); |
|
length -= cnt; |
|
} |
|
} |
|
|
|
|
|
/* update and get the available space on h/w buffer (in frames) */ |
|
snd_pcm_sframes_t AlsaAudio::getAvailableFrames( void ) |
|
{ |
|
snd_pcm_sframes_t ret; |
|
|
|
if ( alsa_pcm == NULL ) |
|
return 0; |
|
|
|
while ( ( ret = snd_pcm_avail_update( alsa_pcm ) ) < 0 ) |
|
{ |
|
ret = alsa_handle_error( ret ); |
|
if ( ret < 0 ) |
|
{ |
|
LOGL( 1, "alsa_get_avail(): snd_pcm_avail_update() failed: " << snd_strerror( -ret ) ); |
|
return 0; |
|
} |
|
return 0; |
|
} |
|
return ret; |
|
} |
|
|
|
|
|
/* transfer data to audio h/w; length is given in bytes |
|
* |
|
* data can be modified via rate conversion or |
|
* software volume before passed to audio h/w |
|
*/ |
|
void AlsaAudio::convertData( void* data, int length ) |
|
{ |
|
if ( alsa_convert_func != NULL ) |
|
length = alsa_convert_func( convertb, &data, length ); |
|
if ( alsa_stereo_convert_func != NULL ) |
|
length = alsa_stereo_convert_func( convertb, &data, length ); |
|
if ( alsa_frequency_convert_func != NULL ) |
|
{ |
|
length = alsa_frequency_convert_func( convertb, &data, length, |
|
inputf->rate, |
|
outputf->rate ); |
|
} |
|
|
|
adjustVolume( data, length, outputf->xmms_format ); |
|
|
|
writeToCard( (char*)data, length ); |
|
} |
|
|
|
|
|
#define VOLUME_ADJUST( type, endian ) \ |
|
do { \ |
|
type *ptr = (type*)data; \ |
|
for ( i = 0; i < length; i += 2 ) \ |
|
{ \ |
|
*ptr = qTo##endian( (type)( qFrom##endian( *ptr ) * volume ) ); \ |
|
ptr++; \ |
|
} \ |
|
} while ( 0 ) |
|
|
|
#define VOLUME_ADJUST8( type ) \ |
|
do { \ |
|
type *ptr = (type*)data; \ |
|
for ( i = 0; i < length; i++ ) \ |
|
{ \ |
|
*ptr = (type)( *ptr * volume ); \ |
|
ptr++; \ |
|
} \ |
|
} while ( 0 ) |
|
|
|
void AlsaAudio::adjustVolume( void* data, int length, AFormat fmt ) |
|
{ |
|
int i; |
|
if ( volume == 1.0 ) |
|
return; |
|
|
|
switch ( fmt ) |
|
{ |
|
case FMT_S16_LE: |
|
VOLUME_ADJUST( qint16, LittleEndian ); |
|
break; |
|
case FMT_U16_LE: |
|
VOLUME_ADJUST( quint16, LittleEndian ); |
|
break; |
|
case FMT_S16_BE: |
|
VOLUME_ADJUST( qint16, BigEndian ); |
|
break; |
|
case FMT_U16_BE: |
|
VOLUME_ADJUST( quint16, BigEndian ); |
|
break; |
|
case FMT_S8: |
|
VOLUME_ADJUST8( qint8 ); |
|
break; |
|
case FMT_U8: |
|
VOLUME_ADJUST8( quint8 ); |
|
break; |
|
default: |
|
LOGL( 1, "AlsaAudio::adjustVolume(): unhandled format: " << fmt ); |
|
break; |
|
} |
|
} |
|
|
|
|
|
/* transfer data to audio h/w via normal write */ |
|
void AlsaAudio::writeToCard( char *data, int length ) |
|
{ |
|
snd_pcm_sframes_t written_frames; |
|
|
|
while ( length > 0 ) |
|
{ |
|
int frames = snd_pcm_bytes_to_frames( alsa_pcm, length ); |
|
|
|
if ( use_mmap ) |
|
{ |
|
written_frames = snd_pcm_mmap_writei( alsa_pcm, data, frames ); |
|
} |
|
else |
|
written_frames = snd_pcm_writei( alsa_pcm, data, frames ); |
|
|
|
if ( written_frames > 0 ) |
|
{ |
|
int written = snd_pcm_frames_to_bytes( alsa_pcm, written_frames ); |
|
length -= written; |
|
data += written; |
|
} |
|
else |
|
{ |
|
int err = alsa_handle_error( (int)written_frames ); |
|
if ( err < 0 ) |
|
{ |
|
LOGL( 1, "AlsaAudio::writeToCard(): write error: " << snd_strerror( -err ) ); |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
/* handle generic errors */ |
|
int AlsaAudio::alsa_handle_error( int err ) |
|
{ |
|
switch ( err ) |
|
{ |
|
case -EPIPE: |
|
return xrun_recover(); |
|
case -ESTRPIPE: |
|
return suspend_recover(); |
|
} |
|
|
|
return err; |
|
} |
|
|
|
|
|
/* close PCM and release associated resources */ |
|
void AlsaAudio::alsa_close_pcm( void ) |
|
{ |
|
if ( alsa_pcm ) |
|
{ |
|
int err; |
|
snd_pcm_drop( alsa_pcm ); |
|
if ( ( err = snd_pcm_close( alsa_pcm ) ) < 0 ) |
|
LOGL( 1, "alsa_pcm_close() failed: " << snd_strerror( -err ) ); |
|
alsa_pcm = NULL; |
|
} |
|
} |
|
|
|
|
|
int AlsaAudio::format_from_alsa( snd_pcm_format_t fmt ) |
|
{ |
|
uint i; |
|
for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) |
|
if ( format_table[i].alsa == fmt ) |
|
return format_table[i].xmms; |
|
LOGL( 1, "Unsupported format: " << snd_pcm_format_name( fmt ) ); |
|
return -1; |
|
} |
|
|
|
struct snd_format * AlsaAudio::snd_format_from_xmms( AFormat fmt, int rate, int channels ) |
|
{ |
|
struct snd_format *f = (struct snd_format*)malloc( sizeof( struct snd_format ) ); |
|
uint i; |
|
|
|
f->xmms_format = fmt; |
|
f->format = SND_PCM_FORMAT_UNKNOWN; |
|
|
|
for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) |
|
{ |
|
if ( format_table[i].xmms == fmt ) |
|
{ |
|
f->format = format_table[i].alsa; |
|
break; |
|
} |
|
} |
|
|
|
/* Get rid of _NE */ |
|
for ( i = 0; i < sizeof( format_table ) / sizeof( format_table[0] ); i++ ) |
|
{ |
|
if ( format_table[i].alsa == f->format ) |
|
{ |
|
f->xmms_format = format_table[i].xmms; |
|
break; |
|
} |
|
} |
|
|
|
f->rate = rate; |
|
f->channels = channels; |
|
f->sample_bits = snd_pcm_format_physical_width( f->format ); |
|
f->bps = ( rate * f->sample_bits * channels ) >> 3; |
|
|
|
return f; |
|
} |
|
|
|
|
|
int AlsaAudio::xrun_recover( void ) |
|
{ |
|
|
|
#ifndef QT_NO_DEBUG |
|
snd_pcm_status_t *alsa_status; |
|
snd_pcm_status_alloca( &alsa_status ); |
|
if ( snd_pcm_status( alsa_pcm, alsa_status ) < 0 ) |
|
{ |
|
qDebug() << "AlsaAudio::xrun_recover(): snd_pcm_status() failed"; |
|
} |
|
else |
|
{ |
|
snd_pcm_status_dump( alsa_status, logs ); |
|
qDebug() << "Status:\n" << logs; |
|
} |
|
#endif |
|
|
|
return snd_pcm_prepare( alsa_pcm ); |
|
} |
|
|
|
int AlsaAudio::suspend_recover( void ) |
|
{ |
|
int err; |
|
|
|
while ( ( err = snd_pcm_resume( alsa_pcm ) ) == -EAGAIN ) |
|
/* wait until suspend flag is released */ |
|
sleep( 1 ); |
|
if ( err < 0 ) |
|
{ |
|
LOGL( 3, "alsa_handle_error(): snd_pcm_resume() failed." ); |
|
return snd_pcm_prepare( alsa_pcm ); |
|
} |
|
return err; |
|
} |