--- a/src/output/alsa-playback/alsaaudio.cpp +++ b/src/output/alsa-playback/alsaaudio.cpp @@ -36,10 +36,13 @@ #define snd_pcm_dump( x, y ) -QMutex AlsaAudio::mutex; pthread_t AlsaAudio::audio_thread; -QByteArray AlsaAudio::audioData; +char* AlsaAudio::thread_buffer = NULL; +int AlsaAudio::thread_buffer_size = 0; +int AlsaAudio::rd_index = 0; +int AlsaAudio::wr_index = 0; + snd_output_t* AlsaAudio::logs = NULL; bool AlsaAudio::going = false; snd_pcm_t *AlsaAudio::alsa_pcm = NULL; @@ -53,13 +56,9 @@ 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() { - m_zero_pad = false; - m_max_buffer_size = 0; } @@ -83,6 +82,12 @@ int err = 0; m_devices.clear(); + // First add the default PCM device + AlsaDeviceInfo dev; + dev.name = "Default PCM device (default)"; + dev.device = "default"; + m_devices.push_back( dev ); + if ((err = snd_card_next( &card )) != 0) goto getCardsFailed; @@ -91,9 +96,9 @@ if ((err = snd_card_next( &card )) != 0) goto getCardsFailed; } - + return m_devices.size(); - + getCardsFailed: qDebug() << __PRETTY_FUNCTION__ << "failed: " << snd_strerror( -err ); return -1; @@ -123,18 +128,6 @@ else cardName = alsa_name; - // 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( cardName ).arg( card ); - dev.device = "default:" + QString::number(card); - snd_pcm_t *test_pcm; - err = snd_pcm_open( &test_pcm, dev.device.toAscii(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK ); - if (err >= 0) - snd_pcm_close( test_pcm ); - if (err == 0 || err == -EBUSY) - m_devices.push_back( dev ); - snd_pcm_info_alloca( &pcm_info ); for (;;) @@ -155,11 +148,12 @@ { if ( err != -ENOENT ) qDebug() << "Failed: snd_ctl_pcm_info() failed" - "(" << card << ":" << pcm_device << "): " + "(" << card << ":" << pcm_device << "): " << snd_strerror( -err ); continue; } + AlsaDeviceInfo dev; dev.device = QString( "hw:%1,%2" ) .arg( card ) .arg( pcm_device ); @@ -167,7 +161,7 @@ .arg( cardName ) .arg( snd_pcm_info_get_name( pcm_info ) ) .arg( dev.device ); - + m_devices.push_back( dev ); } @@ -188,22 +182,18 @@ bool AlsaAudio::alsaOpen( QString device, AFormat format, unsigned int rate, unsigned int channels, snd_pcm_uframes_t periodSize, - unsigned int periodCount ) + unsigned int periodCount, int minBufferCapacity ) { - int err; + int err, hw_buffer_size; ssize_t 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; Q_DEBUG_BLOCK << "Setting up Device:" << device; inputf = snd_format_from_xmms( format, rate, channels ); - // We'll be using this in alsaWrite - m_max_buffer_size = inputf->bps; - convertb = xmms_convert_buffers_new(); snd_output_stdio_attach( &logs, stderr, 0 ); @@ -219,9 +209,9 @@ qDebug() << "Opening device:" << device; // FIXME: Can snd_pcm_open() return EAGAIN? - if ((err = snd_pcm_open( &alsa_pcm, - device.toAscii(), - SND_PCM_STREAM_PLAYBACK, + if ((err = snd_pcm_open( &alsa_pcm, + device.toAscii(), + SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK )) < 0) { qDebug() << "Failed to open pcm device (" << device << "): " << snd_strerror( -err ); @@ -240,7 +230,7 @@ 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; qDebug() << "Device:" << alsa_device; qDebug() << "Subdevice:" << alsa_subdevice; @@ -249,38 +239,19 @@ if ((err = snd_pcm_hw_params_any( alsa_pcm, hwparams )) < 0) { - qDebug() << "No configuration available for playback: " + qDebug() << "No configuration available for playback: " << snd_strerror( -err ); alsaClose(); 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 ); - - - qDebug() << "Trying to set mmapped write mode"; - - if ( ( err = snd_pcm_hw_params_set_access_mask( alsa_pcm, hwparams, mask ) ) < 0 ) + if ( ( err = snd_pcm_hw_params_set_access( alsa_pcm, hwparams, + SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) { - use_mmap = false; - - qDebug() << "Setting mmapped write mode failed: " << snd_strerror( -err ) << "\n Trying normal write mode"; - - if ( ( err = snd_pcm_hw_params_set_access( alsa_pcm, hwparams, - SND_PCM_ACCESS_RW_INTERLEAVED ) ) < 0 ) - { - qDebug() << "Cannot set normal write mode: " << snd_strerror( -err ); - alsaClose(); - return false; - } + qDebug() << "Cannot set normal write mode: " << snd_strerror( -err ); + alsaClose(); + return false; } - else - use_mmap = true; if ( ( err = snd_pcm_hw_params_set_format( alsa_pcm, hwparams, outputf->format ) ) < 0 ) { @@ -444,7 +415,21 @@ hw_period_size_in = hw_period_size; } + hw_buffer_size = snd_pcm_frames_to_bytes( alsa_pcm, alsa_buffer_size ); + thread_buffer_size = minBufferCapacity * 2; + if (thread_buffer_size < hw_buffer_size) + thread_buffer_size = hw_buffer_size * 2; + if (thread_buffer_size < 8192) + thread_buffer_size = 8192; + thread_buffer_size += hw_buffer_size; + thread_buffer_size -= thread_buffer_size % hw_period_size; + + thread_buffer = (char*)calloc(thread_buffer_size, sizeof(char)); + qDebug() << "Device setup: period size:" << hw_period_size; + qDebug() << "Device setup: hw_period_size_in:" << hw_period_size_in; + qDebug() << "Device setup: hw_buffer_size:" << hw_buffer_size; + qDebug() << "Device setup: thread_buffer_size:" << thread_buffer_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; @@ -464,12 +449,6 @@ if ( !alsa_pcm ) return 1; - //HACK because we should zero pad only after we have started - m_zero_pad = false; - - // And clear the buffer, just in case - clearBuffer(); - going = true; AlsaAudio* aaThread = new AlsaAudio(); @@ -483,42 +462,67 @@ void AlsaAudio::clearBuffer( void ) { - QMutexLocker locker( &mutex ); - audioData.clear(); + wr_index = rd_index = 0; + if ( thread_buffer ) + memset( thread_buffer, 0, thread_buffer_size ); } /****************************************************************************** Play Interface ******************************************************************************/ -void AlsaAudio::alsaWrite( const QByteArray* input ) +void AlsaAudio::alsaWrite( const QByteArray& input ) { - #if 0 - qDebug() << "max buffer size:" << m_max_buffer_size << ';' - << "buffer data:" << audioData.size() << ';' - << "input data:" << input->size(); - #endif - - QMutexLocker locker( &mutex ); - // why would we want to lose frames? - //int const n = m_max_buffer_size * 2 - audioData.size(); - //audioData.append( input->left( n ) ); - audioData += *input; + int cnt; + const char *src = input.data(); + int length = input.size(); + //qDebug() << "alsaWrite length:" << length; + + while (length > 0) + { + int wr; + cnt = qMin(length, thread_buffer_size - wr_index); + memcpy(thread_buffer + wr_index, src, cnt); + wr = (wr_index + cnt) % thread_buffer_size; + wr_index = wr; + length -= cnt; + src += cnt; + } } int -AlsaAudio::bufferSize() const +AlsaAudio::get_thread_buffer_filled() const { - QMutexLocker locker( &mutex ); - return audioData.size(); + if ( wr_index >= rd_index ) + { + return wr_index - rd_index; + } + return ( thread_buffer_size - ( rd_index - wr_index ) ); } -bool -AlsaAudio::needsData() const + +// HACK: the buffer may have data, but not enough to send to the card. In that +// case we tell alsaplayback that we don't have any. This may chop off some +// data, but only at the natural end of a track. On my machine, this is at +// most 3759 bytes. That's less than 0.022 sec. It beats padding the buffer +// with 0's if the stream fails mid track. No stutter this way. +int +AlsaAudio::hasData() +{ + int tempSize = get_thread_buffer_filled(); + if ( tempSize < hw_period_size_in ) + return 0; + else + return tempSize; +} + + +int +AlsaAudio::alsa_free() const { - QMutexLocker locker( &mutex ); - return audioData.size() < m_max_buffer_size; + //qDebug() << "alsa_free:" << thread_buffer_size - get_thread_buffer_filled() - 1; + return thread_buffer_size - get_thread_buffer_filled() - 1; } @@ -554,6 +558,11 @@ xmms_convert_buffers_destroy( convertb ); convertb = NULL; + if ( thread_buffer ) + { + free(thread_buffer); + thread_buffer = NULL; + } if ( inputf ) { free( inputf ); @@ -588,74 +597,46 @@ AlsaAudio::run() { int npfds = snd_pcm_poll_descriptors_count( alsa_pcm ); + int wr = 0; int err; - 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 ); + err = snd_pcm_prepare( alsa_pcm ); if ( err < 0 ) qDebug() << "snd_pcm_prepare error:" << snd_strerror( err ); while ( going && alsa_pcm ) { - if (audioData.size() < hw_period_size_in) - if (m_zero_pad && audioData.size()) + if ( get_thread_buffer_filled() >= hw_period_size_in ) + { + wr = snd_pcm_wait( alsa_pcm, 10 ); + + if ( wr > 0 ) { -// qDebug() << "zeroPadding" << audioData.size(); - - QMutexLocker locker( &mutex ); - audioData = audioData.rightJustified( hw_period_size_in, '\0' ); + alsa_write_out_thread_data(); } - else + else if ( wr < 0 ) { -// qDebug() << "Waiting for more data"; - - struct timespec req; - req.tv_sec = 0; - req.tv_nsec = 10000000; //0.1 seconds - nanosleep( &req, NULL ); + alsa_handle_error( wr ); } - - if (audioData.size() >= hw_period_size_in) + } + else { - - // zero pad next time, as we have now started this track - // NOTE this is a HACK and will cause crap behaviour when the streamer - // fails mid track, and you'll get a stuttered restart after buffering - m_zero_pad = true; - - 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; - } - } + struct timespec req; + req.tv_sec = 0; + req.tv_nsec = 10000000; //0.1 seconds + nanosleep( &req, NULL ); } } - free( pfds ); - free( revents ); _error: err = snd_pcm_drop( alsa_pcm ); if ( err < 0 ) qDebug() << "snd_pcm_drop error:" << snd_strerror( err ); - QMutexLocker locker( &mutex ); - audioData.clear(); - locker.unlock(); + wr_index = rd_index = 0; + memset( thread_buffer, 0, thread_buffer_size ); qDebug() << "Exiting thread"; @@ -664,25 +645,27 @@ /* transfer audio data from thread buffer to h/w */ -void AlsaAudio::pumpThreadData( void ) +void AlsaAudio::alsa_write_out_thread_data( void ) { ssize_t length; - length = qMin( hw_period_size_in, ssize_t(audioData.size()) ); - length = qMin( length, snd_pcm_frames_to_bytes( alsa_pcm, getAvailableFrames() ) ); - - for (ssize_t n = 0; length > 0; length -= n) - { - n = qMin( length, ssize_t(audioData.size()) ); - convertData( audioData.left( n ).data(), n ); - - QMutexLocker locker( &mutex ); - audioData.remove( 0, n ); + int cnt; + length = qMin( hw_period_size_in, ssize_t(get_thread_buffer_filled()) ); + length = qMin( length, snd_pcm_frames_to_bytes( alsa_pcm, alsa_get_avail() ) ); + + while (length > 0) + { + int rd; + cnt = qMin( length, ssize_t(thread_buffer_size - rd_index) ); + alsa_do_write( thread_buffer + rd_index, cnt); + rd = (rd_index + cnt) % thread_buffer_size; + rd_index = rd; + 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 AlsaAudio::alsa_get_avail( void ) { snd_pcm_sframes_t ret; @@ -697,7 +680,6 @@ qDebug() << "alsa_get_avail(): snd_pcm_avail_update() failed: " << snd_strerror( -ret ); return 0; } - return 0; } return ret; } @@ -708,7 +690,7 @@ * data can be modified via rate conversion or * software volume before passed to audio h/w */ -void AlsaAudio::convertData( void* data, ssize_t length ) +void AlsaAudio::alsa_do_write( void* data, ssize_t length ) { if ( alsa_convert_func != NULL ) length = alsa_convert_func( convertb, &data, length ); @@ -721,9 +703,9 @@ outputf->rate ); } - adjustVolume( data, length, outputf->xmms_format ); + volume_adjust( data, length, outputf->xmms_format ); - writeToCard( (char*)data, length ); + alsa_write_audio( (char*)data, length ); } @@ -747,7 +729,7 @@ } \ } while ( 0 ) -void AlsaAudio::adjustVolume( void* data, ssize_t length, AFormat fmt ) +void AlsaAudio::volume_adjust( void* data, ssize_t length, AFormat fmt ) { ssize_t i; if ( volume == 1.0 ) @@ -781,20 +763,14 @@ /* transfer data to audio h/w via normal write */ -void AlsaAudio::writeToCard( char *data, ssize_t length ) +void AlsaAudio::alsa_write_audio( char *data, ssize_t length ) { snd_pcm_sframes_t written_frames; while ( length > 0 ) { snd_pcm_sframes_t 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 ); + written_frames = snd_pcm_writei( alsa_pcm, data, frames ); if ( written_frames > 0 ) { @@ -893,6 +869,7 @@ 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 ) @@ -904,7 +881,7 @@ snd_pcm_status_dump( alsa_status, logs ); qDebug() << "Status:\n" << logs; } - +#endif return snd_pcm_prepare( alsa_pcm ); } --- a/LastFM.pro +++ b/LastFM.pro @@ -35,8 +35,7 @@ SUBDIRS -= src/LastFMHelper \ src/mediadevices/itunes - SUBDIRS += src/output/alsa-playback \ - src/output/portAudio + SUBDIRS += src/output/alsa-playback } --- a/src/output/alsa-playback/alsaaudio.h +++ b/src/output/alsa-playback/alsaaudio.h @@ -25,7 +25,6 @@ #include <QByteArray> #include <QList> #include <QString> -#include <QMutex> #include "xconvert.h" @@ -72,26 +71,22 @@ bool alsaOpen( QString device, AFormat format, unsigned int rate, unsigned int channels, snd_pcm_uframes_t periodSize, - unsigned int periodCount ); + unsigned int periodCount, int minBufferCapacity ); int startPlayback(); - void alsaWrite( const QByteArray* inputData ); + void alsaWrite( const QByteArray& inputData ); void stopPlayback(); void alsaClose(); void setVolume ( float vol ); - int bufferSize() const; - bool needsData() const; + int hasData(); + int get_thread_buffer_filled() const; + //bool needsData() const; + int alsa_free() const; void clearBuffer(); - - void setMaxBufferCapacity( int ) { /*m_max_buffer_size = m;*/ } - - static QByteArray audioData; private: QList<AlsaDeviceInfo> m_devices; - int m_max_buffer_size; - // The following static variables are configured in either // alsaOpen or alsaSetup and used later in the audio thread static ssize_t hw_period_size_in; @@ -106,20 +101,22 @@ static convert_freq_func_t alsa_frequency_convert_func; static xmms_convert_buffers *convertb; static pthread_t audio_thread; - static QMutex mutex; - static bool use_mmap; void getDevicesForCard( int card ); - bool alsaSetup( QString device, snd_pcm_uframes_t periodSize, uint periodCount, snd_format *f ); static void* alsa_loop( void* ); void run(); - void pumpThreadData(); - void convertData( void* data, ssize_t length ); - void adjustVolume( void* data, ssize_t length, AFormat fmt ); - void writeToCard( char *data, ssize_t length ); + void alsa_write_out_thread_data(); + void alsa_do_write( void* data, ssize_t length ); + void volume_adjust( void* data, ssize_t length, AFormat fmt ); + void alsa_write_audio( char *data, ssize_t length ); + //int get_thread_buffer_filled() const; + + static char* thread_buffer; + static int thread_buffer_size; + static int rd_index, wr_index; - snd_pcm_sframes_t getAvailableFrames( void ); + snd_pcm_sframes_t alsa_get_avail( void ); int alsa_handle_error( int err ); int xrun_recover(); int suspend_recover(); @@ -127,8 +124,6 @@ snd_format* snd_format_from_xmms( AFormat fmt, unsigned int rate, unsigned int channels ); void alsa_close_pcm( void ); - - bool m_zero_pad; }; #endif --- a/src/output/alsa-playback/alsaplayback.cpp +++ b/src/output/alsa-playback/alsaplayback.cpp @@ -32,6 +32,7 @@ AlsaPlayback::AlsaPlayback() : m_audio( 0 ) + , m_bufferCapacity( 0 ) { initAudio( 44100, 2 ); } @@ -46,28 +47,29 @@ bool AlsaPlayback::hasData() { - return m_audio->bufferSize(); + bool has = m_audio->hasData() > 0; + return has; } bool AlsaPlayback::needsData() { - return m_audio->needsData(); + return ( m_audio->get_thread_buffer_filled() < m_bufferCapacity ); } void AlsaPlayback::setBufferCapacity( int size ) { - m_audio->setMaxBufferCapacity( size ); + m_bufferCapacity = size; } int AlsaPlayback::bufferSize() { - return m_audio->bufferSize(); + return m_audio->get_thread_buffer_filled(); } @@ -150,9 +152,9 @@ // We assume host byte order #ifdef WORDS_BIGENDIAN - if (!m_audio->alsaOpen( cardDevice, FMT_S16_BE, sampleRate, channels, periodSize, periodCount )) + if (!m_audio->alsaOpen( cardDevice, FMT_S16_BE, sampleRate, channels, periodSize, periodCount, m_bufferCapacity )) #else - if (!m_audio->alsaOpen( cardDevice, FMT_S16_LE, sampleRate, channels, periodSize, periodCount )) + if (!m_audio->alsaOpen( cardDevice, FMT_S16_LE, sampleRate, channels, periodSize, periodCount, m_bufferCapacity )) #endif goto _error; @@ -170,7 +172,7 @@ void AlsaPlayback::processData( const QByteArray &buffer ) { - m_audio->alsaWrite( &buffer ); + m_audio->alsaWrite( buffer ); } --- a/src/output/alsa-playback/alsaplayback.h +++ b/src/output/alsa-playback/alsaplayback.h @@ -62,6 +62,7 @@ private: class AlsaAudio *m_audio; + int m_bufferCapacity; static float m_volume;